diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index aca232471d6..ba3fc9077e8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -1860,11 +1860,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { 0xa0 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[0].splice_channel( &chan_a_id, @@ -1882,11 +1879,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa1 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( &chan_a_id, @@ -1904,11 +1898,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa2 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( &chan_b_id, @@ -1926,11 +1917,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa3 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[2].splice_channel( &chan_b_id, @@ -1958,12 +1946,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[0].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[0].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[0].splice_channel( @@ -1989,12 +1975,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( @@ -2020,12 +2004,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( @@ -2051,12 +2033,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[2].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[2].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[2].splice_channel( diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs index cef180fbd4e..924c6c860e1 100644 --- a/lightning-tests/src/upgrade_downgrade_tests.rs +++ b/lightning-tests/src/upgrade_downgrade_tests.rs @@ -441,12 +441,10 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) { reconnect_b_c_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_b_c_args); - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); let splice_tx = splice_channel(&nodes[0], &nodes[1], ChannelId(chan_id_bytes_a), contribution); for node in nodes.iter() { mine_transaction(node, &splice_tx); diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 3d9beb82c07..2404ae98611 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -29,6 +29,7 @@ use crate::ln::chan_utils::{ HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, P2WSH_TXOUT_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, TRUC_CHILD_MAX_WEIGHT, TRUC_MAX_WEIGHT, }; +use crate::ln::funding::FundingTxInput; use crate::ln::types::ChannelId; use crate::prelude::*; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -269,6 +270,12 @@ pub struct Input { pub satisfaction_weight: u64, } +impl_writeable_tlv_based!(Input, { + (1, outpoint, required), + (3, previous_utxo, required), + (5, satisfaction_weight, required), +}); + /// An unspent transaction output that is available to spend resulting from a successful /// [`CoinSelection`] attempt. #[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -281,12 +288,15 @@ pub struct Utxo { /// with their lengths included, required to satisfy the output's script. The weight consumed by /// the input's `script_sig` must account for [`WITNESS_SCALE_FACTOR`]. pub satisfaction_weight: u64, + /// The sequence number to use in the [`TxIn`] when spending the UTXO. + pub sequence: Sequence, } impl_writeable_tlv_based!(Utxo, { (1, outpoint, required), (3, output, required), (5, satisfaction_weight, required), + (7, sequence, (default_value, Sequence::ENABLE_RBF_NO_LOCKTIME)), }); impl Utxo { @@ -301,6 +311,7 @@ impl Utxo { outpoint, output: TxOut { value, script_pubkey: ScriptBuf::new_p2pkh(pubkey_hash) }, satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + 1, /* empty witness */ + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } @@ -320,6 +331,7 @@ impl Utxo { }, satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } @@ -329,17 +341,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 @@ -347,6 +363,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`], @@ -413,9 +439,14 @@ pub trait CoinSelectionSource { pub trait WalletSource { /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. fn list_confirmed_utxos<'a>(&'a self) -> AsyncResult<'a, Vec, ()>; + + /// Returns the previous transaction containing the UTXO. + fn get_prevtx<'a>(&'a self, utxo: &Utxo) -> AsyncResult<'a, Transaction, ()>; + /// Returns a script to use for change above dust resulting from a successful coin selection /// attempt. fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()>; + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within /// the transaction known to the wallet (i.e., any provided via /// [`WalletSource::list_confirmed_utxos`]). @@ -601,10 +632,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 }) } } @@ -713,11 +747,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(), }); } @@ -835,12 +869,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(); @@ -855,10 +887,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()); } } @@ -1090,13 +1122,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; @@ -1117,9 +1147,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()); } } @@ -1263,9 +1296,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) @@ -1286,12 +1318,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) } } @@ -1328,6 +1358,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![ @@ -1342,13 +1379,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 653710a3358..75be588848a 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -36,9 +36,14 @@ use super::{ pub trait WalletSourceSync { /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. fn list_confirmed_utxos(&self) -> Result, ()>; + + /// Returns the previous transaction containing the UTXO. + fn get_prevtx(&self, utxo: &Utxo) -> Result; + /// Returns a script to use for change above dust resulting from a successful coin selection /// attempt. fn get_change_script(&self) -> Result; + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within /// the transaction known to the wallet (i.e., any provided via /// [`WalletSource::list_confirmed_utxos`]). @@ -76,6 +81,11 @@ where Box::pin(async move { utxos }) } + fn get_prevtx<'a>(&'a self, utxo: &Utxo) -> AsyncResult<'a, Transaction, ()> { + let prevtx = self.0.get_prevtx(utxo); + Box::pin(async move { prevtx }) + } + fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()> { let script = self.0.get_change_script(); Box::pin(async move { script }) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index b9c4b1ca1ef..6b55a24a622 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -25,6 +25,7 @@ use crate::blinded_path::payment::{ use crate::chain::transaction; use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; +use crate::ln::funding::FundingTemplate; use crate::ln::types::ChannelId; use crate::ln::{msgs, LocalHTLCFailureReason}; use crate::offers::invoice::Bolt12Invoice; @@ -1816,6 +1817,28 @@ pub enum Event { /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request invoice_request: InvoiceRequest, }, + /// + FundingNeeded { + /// The `channel_id` of the channel which you'll need to pass back into + /// [`ChannelManager::funding_contributed`]. + /// + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + channel_id: ChannelId, + /// The counterparty's `node_id`, which you'll need to pass back into + /// [`ChannelManager::funding_contributed`]. + /// + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + counterparty_node_id: PublicKey, + /// The `user_channel_id` value passed in for outbound channels, or for inbound channels if + /// [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. Otherwise + /// `user_channel_id` will be randomized for inbound channels. + /// + /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels + user_channel_id: u128, + + /// + funding_template: FundingTemplate, + }, /// Indicates that a channel funding transaction constructed interactively is ready to be /// signed. This event will only be triggered if at least one input was contributed. /// @@ -2347,6 +2370,20 @@ impl Writeable for Event { (13, *contributed_outputs, optional_vec), }); }, + &Event::FundingNeeded { + ref channel_id, + ref user_channel_id, + ref counterparty_node_id, + ref funding_template, + } => { + 54u8.write(writer)?; + write_tlv_fields!(writer, { + (1, channel_id, required), + (3, user_channel_id, required), + (5, counterparty_node_id, required), + (7, funding_template, required), + }); + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2978,6 +3015,24 @@ impl MaybeReadable for Event { }; f() }, + 54u8 => { + let mut f = || { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (1, channel_id, required), + (3, user_channel_id, required), + (5, counterparty_node_id, required), + (7, funding_template, required), + }); + + Ok(Some(Event::FundingNeeded { + channel_id: channel_id.0.unwrap(), + user_channel_id: user_channel_id.0.unwrap(), + counterparty_node_id: counterparty_node_id.0.unwrap(), + funding_template: funding_template.0.unwrap(), + })) + }; + f() + }, // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5b4ac4c0aa5..07315debb71 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11,7 +11,7 @@ use bitcoin::absolute::LockTime; use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::encode; use bitcoin::constants::ChainHash; -use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash}; +use bitcoin::script::{Builder, Script, ScriptBuf}; use bitcoin::sighash::EcdsaSighashType; use bitcoin::transaction::{Transaction, TxOut}; use bitcoin::Witness; @@ -24,7 +24,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn}; +use bitcoin::{secp256k1, sighash, Sequence, TxIn}; use crate::blinded_path::message::BlindedMessagePath; use crate::chain::chaininterface::{ @@ -37,13 +37,14 @@ use crate::chain::channelmonitor::{ use crate::chain::transaction::{OutPoint, TransactionData}; use crate::chain::BestBlock; use crate::events::{ClosureReason, FundingInfo}; +use crate::events::bump_transaction::Input; use crate::ln::chan_utils; use crate::ln::chan_utils::{ get_commitment_transaction_number_obscure_factor, max_htlcs, second_stage_tx_fees_sat, selected_commitment_sat_per_1000_weight, ChannelPublicKeys, ChannelTransactionParameters, ClosingTransaction, CommitmentTransaction, CounterpartyChannelTransactionParameters, CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HolderCommitmentTransaction, - BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, + EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, }; use crate::ln::channel_state::{ ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails, @@ -55,12 +56,11 @@ use crate::ln::channelmanager::{ RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::{FundingTemplate, FundingContribution, FundingTxInput, SpliceContribution}; use crate::ln::interactivetxs::{ calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, NegotiationError, SharedOwnedInput, SharedOwnedOutput, - TX_COMMON_FIELDS_WEIGHT, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; @@ -69,7 +69,6 @@ use crate::ln::onion_utils::{ }; use crate::ln::script::{self, ShutdownScript}; use crate::ln::types::ChannelId; -use crate::ln::LN_MAX_MSG_LEN; use crate::offers::static_invoice::StaticInvoice; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -2484,6 +2483,20 @@ impl FundingScope { self.channel_transaction_parameters.funding_outpoint } + /// Gets the funding output for this channel, if available. + /// + /// When a channel is spliced, this continues to refer to the original funding output (which + /// was spent by the splice transaction) until the splice transaction reaches sufficient + /// confirmations to be locked (and we exchange `splice_locked` messages with our peer). + pub fn get_funding_output(&self) -> Option { + self.channel_transaction_parameters + .make_funding_redeemscript_opt() + .and_then(|redeem_script| Some(TxOut { + value: Amount::from_sat(self.get_value_satoshis()), + script_pubkey: redeem_script.to_p2wsh(), + })) + } + fn get_funding_txid(&self) -> Option { self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) } @@ -2802,7 +2815,8 @@ impl_writeable_tlv_based!(SpliceInstructions, { #[derive(Debug)] pub(crate) enum QuiescentAction { - Splice(SpliceInstructions), + LegacySplice(SpliceInstructions), + Splice(SpliceContribution), #[cfg(any(test, fuzzing))] DoNothing, } @@ -2810,16 +2824,19 @@ pub(crate) enum QuiescentAction { pub(crate) enum StfuResponse { Stfu(msgs::Stfu), SpliceInit(msgs::SpliceInit), + FundingNeeded(FundingTemplate), } #[cfg(any(test, fuzzing))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, (0, DoNothing) => {}, - {1, Splice} => (), + {1, LegacySplice} => (), + {2, Splice} => (), ); #[cfg(not(any(test, fuzzing)))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,, - {1, Splice} => (), + {1, LegacySplice} => (), + {2, Splice} => (), ); /// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. @@ -6494,128 +6511,6 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } -fn check_splice_contribution_sufficient( - contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate, -) -> Result { - let contribution_amount = contribution.value(); - if contribution_amount < SignedAmount::ZERO { - 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, - )); - - contribution_amount - .checked_sub( - estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"), - ) - .ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply")) - } else { - check_v2_funding_inputs_sufficient( - contribution_amount.to_sat(), - contribution.inputs(), - is_initiator, - true, - funding_feerate.to_sat_per_kwu() as u32, - ) - .map(|_| contribution_amount) - } -} - -/// 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( - contribution_amount: i64, funding_inputs: &[FundingTxInput], is_initiator: bool, - is_splice: bool, funding_feerate_sat_per_1000_weight: u32, -) -> Result { - let estimated_fee = estimate_v2_funding_transaction_fee( - funding_inputs, &[], is_initiator, is_splice, funding_feerate_sat_per_1000_weight, - ); - - let mut total_input_sats = 0u64; - for FundingTxInput { utxo, .. } in funding_inputs.iter() { - total_input_sats = total_input_sats.checked_add(utxo.output.value.to_sat()) - .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 = contribution_amount.checked_add(estimated_fee as i64) - .ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))?; - if i64::try_from(total_input_sats).map_err(|_| "Sum of input values is greater than the total bitcoin supply")? - < minimal_input_amount_needed - { - Err(format!( - "Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, 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 { @@ -6675,7 +6570,7 @@ impl FundingNegotiationContext { }; // Optionally add change output - let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO { + let change_value_opt = if !self.our_funding_inputs.is_empty() { match calculate_change_output_value( &self, self.shared_funding_input.is_some(), @@ -6703,12 +6598,12 @@ impl FundingNegotiationContext { }, } }; - let mut change_output = - TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script }; + let mut change_output = TxOut { value: change_value, script_pubkey: change_script }; let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); let change_output_fee = fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight); - let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee); + let change_value_decreased_with_fee = + change_value.to_sat().saturating_sub(change_output_fee); // Check dust limit again if change_value_decreased_with_fee > context.holder_dust_limit_satoshis { change_output.value = Amount::from_sat(change_value_decreased_with_fee); @@ -6967,7 +6862,7 @@ where self.reset_pending_splice_state() } else { match self.quiescent_action.take() { - Some(QuiescentAction::Splice(instructions)) => { + Some(QuiescentAction::LegacySplice(instructions)) => { self.context.channel_state.clear_awaiting_quiescence(); let (inputs, outputs) = instructions.into_contributed_inputs_and_outputs(); Some(SpliceFundingFailed { @@ -6977,6 +6872,15 @@ where contributed_outputs: outputs, }) }, + Some(QuiescentAction::Splice(contribution)) => { + self.context.channel_state.clear_awaiting_quiescence(); + Some(SpliceFundingFailed { + funding_txo: None, + channel_type: None, + contributed_inputs: vec![], + contributed_outputs: contribution.into_outputs(), + }) + }, #[cfg(any(test, fuzzing))] Some(quiescent_action) => { self.quiescent_action = Some(quiescent_action); @@ -11271,7 +11175,7 @@ where self.get_announcement_sigs(node_signer, chain_hash, user_config, block_height, logger); if let Some(quiescent_action) = self.quiescent_action.as_ref() { - if matches!(quiescent_action, QuiescentAction::Splice(_)) { + if matches!(quiescent_action, QuiescentAction::Splice(_) | QuiescentAction::LegacySplice(_)) { self.context.channel_state.set_awaiting_quiescence(); } } @@ -11921,8 +11825,7 @@ where /// - `change_script`: an option change output script. If `None` and needed, one will be /// generated by `SignerProvider::get_destination_script`. pub fn splice_channel( - &mut self, contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: u32, - logger: &L, + &mut self, contribution: SpliceContribution, logger: &L, ) -> Result, APIError> where L::Target: Logger, @@ -11936,8 +11839,15 @@ where }); } - // Check if a splice has been initiated already. - // Note: only a single outstanding splice is supported (per spec) + if self.context.channel_state.is_quiescent() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced as it is already quiescent", + self.context.channel_id(), + ), + }); + } + if self.pending_splice.is_some() || self.quiescent_action.is_some() { return Err(APIError::APIMisuseError { err: format!( @@ -11956,7 +11866,7 @@ where }); } - let our_funding_contribution = contribution.value(); + let our_funding_contribution = contribution.net_value(); if our_funding_contribution == SignedAmount::ZERO { return Err(APIError::APIMisuseError { err: format!( @@ -11966,71 +11876,67 @@ where }); } - // Fees for splice-out are paid from the channel balance whereas fees for splice-in - // are paid by the funding inputs. Therefore, in the case of splice-out, we add the - // fees on top of the user-specified contribution. We leave the user-specified - // contribution as-is for splice-ins. - let adjusted_funding_contribution = check_splice_contribution_sufficient( - &contribution, - true, - FeeRate::from_sat_per_kwu(u64::from(funding_feerate_per_kw)), - ) - .map_err(|e| APIError::APIMisuseError { - err: format!( - "Channel {} cannot be {}; {}", - self.context.channel_id(), - if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, - e - ), - })?; - // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known // (Cannot test for miminum required post-splice channel value) let their_funding_contribution = SignedAmount::ZERO; self.validate_splice_contributions( - adjusted_funding_contribution, + our_funding_contribution, their_funding_contribution, ) .map_err(|err| APIError::APIMisuseError { err })?; - for FundingTxInput { utxo, prevtx, .. } in contribution.inputs().iter() { - const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { - channel_id: ChannelId([0; 32]), - serial_id: 0, - prevtx: None, - prevtx_out: 0, - sequence: 0, - // Mutually exclusive with prevtx, which is accounted for below. - shared_input_txid: None, - }; - let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); - if message_len > LN_MAX_MSG_LEN { - return Err(APIError::APIMisuseError { - err: format!( - "Funding input references a prevtx that is too large for tx_add_input: {}", - utxo.outpoint, - ), + self.propose_quiescence(logger, QuiescentAction::Splice(contribution)) + .map_err(|e| APIError::APIMisuseError { err: e.to_owned() }) + } + + pub fn funding_contributed( + &mut self, contribution: FundingContribution, locktime: u32, logger: &L, + ) -> Result + where + L::Target: Logger, + { + let adjusted_funding_contribution = match contribution.validate() { + Ok(adjusted_contribution) => adjusted_contribution, + Err(e) => { + log_error!( + logger, + "Channel {} cannot be {}; {}", + self.context.channel_id(), + if contribution.net_value().is_positive() { "spliced in" } else { "spliced out" }, + e + ); + + let (contributed_inputs, contributed_outputs) = contribution.into_contributed_inputs_and_outputs(); + + return Err(SpliceFundingFailed { + funding_txo: None, + channel_type: None, + contributed_inputs, + contributed_outputs, }); - } - } + }, + }; + let prev_funding_input = self.funding.to_splice_funding_input(); + let is_initiator = contribution.is_initiator(); + let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32; let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts(); - let action = QuiescentAction::Splice(SpliceInstructions { - adjusted_funding_contribution, + let context = FundingNegotiationContext { + is_initiator, + our_funding_contribution: adjusted_funding_contribution, + funding_tx_locktime: LockTime::from_consensus(locktime), + funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, + shared_funding_input: Some(prev_funding_input), our_funding_inputs, our_funding_outputs, change_script, - funding_feerate_per_kw, - locktime, - }); - self.propose_quiescence(logger, action) - .map_err(|e| APIError::APIMisuseError { err: e.to_owned() }) + }; + + Ok(self.send_splice_init_internal(context)) } fn send_splice_init(&mut self, instructions: SpliceInstructions) -> msgs::SpliceInit { - debug_assert!(self.pending_splice.is_none()); - let SpliceInstructions { adjusted_funding_contribution, our_funding_inputs, @@ -12052,6 +11958,11 @@ where change_script, }; + self.send_splice_init_internal(context) + } + + fn send_splice_init_internal(&mut self, context: FundingNegotiationContext) -> msgs::SpliceInit { + debug_assert!(self.pending_splice.is_none()); // Rotate the funding pubkey using the prev_funding_txid as a tweak let prev_funding_txid = self.funding.get_funding_txid(); let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) { @@ -12066,6 +11977,10 @@ where _ => todo!(), }; + let funding_feerate_per_kw = context.funding_feerate_sat_per_1000_weight; + let funding_contribution_satoshis = context.our_funding_contribution.to_sat(); + let locktime = context.funding_tx_locktime.to_consensus_u32(); + let funding_negotiation = FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey }; self.pending_splice = Some(PendingFunding { @@ -12077,7 +11992,7 @@ where msgs::SpliceInit { channel_id: self.context.channel_id, - funding_contribution_satoshis: adjusted_funding_contribution.to_sat(), + funding_contribution_satoshis, funding_feerate_per_kw, locktime, funding_pubkey, @@ -13299,9 +13214,9 @@ where "Internal Error: Didn't have anything to do after reaching quiescence".to_owned() )); }, - Some(QuiescentAction::Splice(instructions)) => { + Some(QuiescentAction::LegacySplice(instructions)) => { if self.pending_splice.is_some() { - self.quiescent_action = Some(QuiescentAction::Splice(instructions)); + self.quiescent_action = Some(QuiescentAction::LegacySplice(instructions)); return Err(ChannelError::WarnAndDisconnect( format!( @@ -13314,6 +13229,29 @@ where let splice_init = self.send_splice_init(instructions); return Ok(Some(StfuResponse::SpliceInit(splice_init))); }, + Some(QuiescentAction::Splice(contribution)) => { + if self.pending_splice.is_some() { + self.quiescent_action = Some(QuiescentAction::Splice(contribution)); + + return Err(ChannelError::WarnAndDisconnect( + format!( + "Channel {} cannot be spliced as it already has a splice pending", + self.context.channel_id(), + ), + )); + } + + let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set"); + let previous_utxo = self.funding.get_funding_output().expect("funding_output should be set"); + let shared_input = Input { + outpoint: funding_txo.into_bitcoin_outpoint(), + previous_utxo, + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT, + }; + + let funding_template = FundingTemplate::for_splice(contribution, shared_input); + return Ok(Some(StfuResponse::FundingNeeded(funding_template))); + }, #[cfg(any(test, fuzzing))] Some(QuiescentAction::DoNothing) => { // In quiescence test we want to just hang out here, letting the test manually @@ -15820,7 +15758,6 @@ mod tests { }; use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey}; use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; - use crate::ln::funding::FundingTxInput; use crate::ln::msgs; use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT}; use crate::ln::onion_utils::{AttributionData, LocalHTLCFailureReason}; @@ -15852,7 +15789,7 @@ mod tests { use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{Transaction, TxOut, Version}; - use bitcoin::{ScriptBuf, WPubkeyHash, WitnessProgram, WitnessVersion}; + use bitcoin::{WitnessProgram, WitnessVersion}; use std::cmp; #[test] @@ -18189,172 +18126,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() - } - - #[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( - 220_000, - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - true, - true, - 2000, - ).unwrap(), - 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( - 220_000, - &[ - funding_input_sats(100_000), - ], - true, - true, - 2000, - ), - Err(format!( - "Total input amount 100000 is lower than needed for contribution 220000, considering fees of {}. Need more inputs.", - expected_fee, - )), - ); - } - - // barely covers - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - (300_000 - expected_fee - 20) as i64, - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - true, - true, - 2000, - ).unwrap(), - 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( - 298032, - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - true, - true, - 2200, - ), - Err(format!( - "Total input amount 300000 is lower than needed for contribution 298032, considering fees of {}. Need more inputs.", - expected_fee - )), - ); - } - - // 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( - (300_000 - expected_fee - 20) as i64, - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - false, - false, - 2000, - ).unwrap(), - expected_fee, - ); - } - } - fn get_pre_and_post( pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, ) -> (u64, u64) { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 72585d69f80..c85940ff3c6 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -63,7 +63,7 @@ use crate::ln::channel::{ WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; -use crate::ln::funding::SpliceContribution; +use crate::ln::funding::{FundingContribution, SpliceContribution}; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; @@ -4743,12 +4743,12 @@ where #[rustfmt::skip] pub fn splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option, + contribution: SpliceContribution, ) -> Result<(), APIError> { let mut res = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { let result = self.internal_splice_channel( - channel_id, counterparty_node_id, contribution, funding_feerate_per_kw, locktime + channel_id, counterparty_node_id, contribution, ); res = result; match res { @@ -4761,7 +4761,7 @@ where fn internal_splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option, + contribution: SpliceContribution, ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -4780,15 +4780,9 @@ where // Look for the channel match peer_state.channel_by_id.entry(*channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let msg_opt = chan.splice_channel( - contribution, - funding_feerate_per_kw, - locktime, - &&logger, - )?; + let msg_opt = chan.splice_channel(contribution, &&logger)?; if let Some(msg) = msg_opt { peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { node_id: *counterparty_node_id, @@ -6417,6 +6411,79 @@ where result } + /// + pub fn funding_contributed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + contribution: FundingContribution, locktime: Option, + ) -> Result<(), APIError> { + let mut result = Ok(()); + PersistenceNotifierGuard::optionally_notify(self, || { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id); + if peer_state_mutex_opt.is_none() { + result = Err(APIError::ChannelUnavailable { + err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") + }); + return NotifyOption::SkipPersistNoEvents; + } + + let mut peer_state = peer_state_mutex_opt.unwrap().lock().unwrap(); + + match peer_state.channel_by_id.get_mut(channel_id) { + Some(channel) => match channel.as_funded_mut() { + Some(chan) => { + let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); + let logger = WithChannelContext::from(&self.logger, chan.context(), None); + match chan.funding_contributed(contribution, locktime, &&logger) { + Ok(msg) => { + peer_state.pending_msg_events.push( + MessageSendEvent::SendSpliceInit { + node_id: *counterparty_node_id, + msg, + }, + ); + }, + Err(splice_funding_failed) => { + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::SpliceFailed { + channel_id: *channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: channel.context().get_user_id(), + abandoned_funding_txo: splice_funding_failed.funding_txo, + channel_type: splice_funding_failed.channel_type.clone(), + contributed_inputs: splice_funding_failed.contributed_inputs, + contributed_outputs: splice_funding_failed.contributed_outputs, + }, None)); + }, + } + + return NotifyOption::DoPersist; + }, + None => { + result = Err(APIError::APIMisuseError { + err: format!( + "Channel with id {} not expecting funding contribution", + channel_id + ), + }); + return NotifyOption::SkipPersistNoEvents; + }, + }, + None => { + result = Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} not found for the passed counterparty node_id {}", + channel_id, counterparty_node_id + ), + }); + return NotifyOption::SkipPersistNoEvents; + }, + } + }); + + result + } + /// Handles a signed funding transaction generated by interactive transaction construction and /// provided by the client. Should only be called in response to a [`FundingTransactionReadyForSigning`] /// event. @@ -11699,6 +11766,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); Ok(true) }, + Some(StfuResponse::FundingNeeded(funding_template)) => { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::FundingNeeded { + channel_id: chan.context.channel_id(), + user_channel_id: chan.context.get_user_id(), + counterparty_node_id: chan.context.get_counterparty_node_id(), + funding_template, + }, + None, + )); + Ok(true) + }, } } else { let msg = "Peer sent `stfu` for an unfunded channel"; diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index ff33d7508b5..c598b83b054 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -395,8 +395,7 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>( let wallet_script = node.wallet_source.get_change_script().unwrap(); for (idx, output) in tx.output.iter().enumerate() { if output.script_pubkey == wallet_script { - let outpoint = bitcoin::OutPoint { txid: tx.compute_txid(), vout: idx as u32 }; - node.wallet_source.add_utxo(outpoint, output.value); + node.wallet_source.add_utxo(tx.clone(), idx as u32); } } } @@ -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_anchor_utxo_reserves(core::slice::from_ref(self), utxos, amount) + } + /// Executes `enable_channel_signer_op` for every single signer operation for this channel. #[cfg(test)] pub fn enable_all_channel_signer_ops(&self, peer_id: &PublicKey, chan_id: &ChannelId) { diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index f80b2b6daea..cd3285b4f66 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -11,78 +11,384 @@ use alloc::vec::Vec; -use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut}; -use bitcoin::{Script, Sequence, Transaction, Weight}; +use bitcoin::{ + Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, SignedAmount, Transaction, TxOut, Weight, + WScriptHash, +}; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; -use crate::events::bump_transaction::Utxo; -use crate::ln::chan_utils::EMPTY_SCRIPT_SIG_WEIGHT; +use core::ops::Deref; + +use crate::chain::ClaimId; +use crate::events::bump_transaction::{CoinSelectionSource, Input, Utxo}; +use crate::events::bump_transaction::sync::CoinSelectionSourceSync; +use crate::ln::chan_utils::{make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT}; +use crate::ln::interactivetxs::{ + get_output_weight, TX_COMMON_FIELDS_WEIGHT, +}; +use crate::ln::msgs; +use crate::ln::types::ChannelId; +use crate::ln::LN_MAX_MSG_LEN; use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +use crate::util::async_poll::AsyncResult; /// The components of a splice's funding transaction that are contributed by one party. #[derive(Debug, Clone)] -pub enum SpliceContribution { - /// When funds are added to a channel. - SpliceIn { - /// The amount to contribute to the splice. - value: Amount, - - /// The inputs included in the splice's funding transaction to meet the contributed amount - /// plus fees. Any excess amount will be sent to a change output. - inputs: Vec, - - /// An optional change output script. This will be used if needed or, when not set, - /// generated using [`SignerProvider::get_destination_script`]. - /// - /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script - change_script: Option, - }, - /// When funds are removed from a channel. - SpliceOut { - /// The outputs to include in the splice's funding transaction. The total value of all - /// outputs plus fees will be the amount that is removed. - outputs: Vec, - }, +pub struct SpliceContribution { + /// The amount of value to contribute from inputs to the splice's funding transaction. + value_added: Amount, + + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs plus fees will be the amount that is removed. + outputs: Vec, + + /// 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), + (5, change_script, option), +}); + impl SpliceContribution { - pub(super) fn value(&self) -> SignedAmount { - match self { - SpliceContribution::SpliceIn { value, .. } => { - value.to_signed().unwrap_or(SignedAmount::MAX) - }, - SpliceContribution::SpliceOut { outputs } => { - let value_removed = outputs - .iter() - .map(|txout| txout.value) - .sum::() - .to_signed() - .unwrap_or(SignedAmount::MAX); - -value_removed - }, - } + /// Creates a contribution for when funds are only added to a channel. + pub fn splice_in( + value_added: Amount, change_script: Option, + ) -> Self { + Self { value_added, outputs: vec![], change_script } + } + + /// Creates a contribution for when funds are only removed from a channel. + pub fn splice_out(outputs: Vec) -> Self { + Self { value_added: Amount::ZERO, outputs, change_script: None } + } + + /// Creates a contribution for when funds are both added to and removed from a channel. + /// + /// Note that `value_added` represents the value added by `inputs` but should not account for + /// value removed by `outputs`. The net value contributed can be obtained by calling + /// [`SpliceContribution::net_value`]. + pub fn splice_in_and_out( + value_added: Amount, outputs: Vec, change_script: Option, + ) -> Self { + Self { value_added, outputs, change_script } + } + + /// The net value contributed to a channel by the splice. If negative, more value will be + /// spliced out than spliced in. + pub fn net_value(&self) -> SignedAmount { + let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX); + let value_removed = self + .outputs + .iter() + .map(|txout| txout.value) + .sum::() + .to_signed() + .unwrap_or(SignedAmount::MAX); + + value_added - value_removed + } + + pub(super) fn into_outputs(self) -> Vec { + self.outputs } - pub(super) fn inputs(&self) -> &[FundingTxInput] { - match self { - SpliceContribution::SpliceIn { inputs, .. } => &inputs[..], - SpliceContribution::SpliceOut { .. } => &[], + #[cfg(test)] + pub(super) fn into_tx_parts(self) -> (Vec, Option) { + (self.outputs, self.change_script) + } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FundingTemplate { + /// The amount from [`inputs`] to contribute to the splice. + /// + /// [`inputs`]: Self::inputs + value_added: Amount, + + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs plus fees will be the amount that is removed. + outputs: Vec, + + change_script: Option, + + shared_input: Option, + + is_initiator: bool, +} + +impl_writeable_tlv_based!(FundingTemplate, { + (1, value_added, required), + (3, outputs, optional_vec), + (5, change_script, option), + (7, shared_input, option), + (9, is_initiator, required), +}); + +impl FundingTemplate { + /// + pub(super) fn for_splice( + contribution: SpliceContribution, shared_input: Input, + ) -> Self { + let SpliceContribution { value_added, outputs, change_script } = contribution; + Self { + value_added, + outputs, + change_script, + shared_input: Some(shared_input), + is_initiator: true, } } - pub(super) fn outputs(&self) -> &[TxOut] { - match self { - SpliceContribution::SpliceIn { .. } => &[], - SpliceContribution::SpliceOut { outputs } => &outputs[..], + /// FIXME: Can these be combined or is a macro needed to DRY them up? + + /// + pub fn build<'a, W: Deref>(self, wallet: W, feerate: FeeRate) -> AsyncResult<'a, FundingContribution, ()> + where + W::Target: CoinSelectionSource, + { + todo!() + } + + /// + pub fn build_sync(self, wallet: W, feerate: FeeRate) -> Result + where + W::Target: CoinSelectionSourceSync, + { + let FundingTemplate { value_added, outputs, change_script, shared_input, is_initiator } = self; + + let value_removed = outputs.iter().map(|txout| txout.value).sum(); + let is_splice = shared_input.is_some(); + + let inputs = if value_added == Amount::ZERO { + vec![] + } else { + // Used for creating a redeem script for the new funding txo, since the funding pubkeys + // are unknown at this point. Only needed when selecting which UTXOs to include in the + // funding tx that would be sufficient to pay for fees. Hence, the value doesn't matter. + let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap(); + + let shared_output = bitcoin::TxOut { + value: shared_input + .as_ref() + .map(|shared_input| shared_input.previous_utxo.value) + .unwrap_or(Amount::ZERO) + .checked_add(value_added) + .ok_or(())? + .checked_sub(value_removed) + .ok_or(())?, + script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(), + }; + + let claim_id = ClaimId([0; 32]); + let must_spend = shared_input.map(|input| vec![input]).unwrap_or_default(); + let selection = if outputs.is_empty() { + let must_pay_to = &[shared_output]; + wallet.select_confirmed_utxos(claim_id, must_spend, must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)? + } else { + let must_pay_to: Vec<_> = outputs.iter().cloned().chain(core::iter::once(shared_output)).collect(); + wallet.select_confirmed_utxos(claim_id, must_spend, &must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)? + }; + selection.confirmed_utxos + }; + + // NOTE: Must NOT fail after UTXO selection + + let estimated_fee = estimate_transaction_fee(&inputs, &outputs, is_initiator, is_splice, feerate); + + let contribution = FundingContribution { + value_added, + value_removed, + estimated_fee, + inputs, + outputs, + change_script, + feerate, + is_initiator, + is_splice, + }; + + Ok(contribution) + } +} + +fn estimate_transaction_fee( + inputs: &[FundingTxInput], outputs: &[TxOut], is_initiator: bool, is_splice: bool, + feerate: FeeRate, +) -> Amount { + let input_weight: u64 = inputs + .iter() + .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight)) + .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight)); + + let output_weight: u64 = outputs + .iter() + .map(|txout| txout.weight().to_wu()) + .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight)); + + let mut weight = input_weight.saturating_add(output_weight); + + // The initiator pays for all common fields and the shared output in the funding transaction. + if is_initiator { + weight = weight + .saturating_add(TX_COMMON_FIELDS_WEIGHT) + // The weight of the funding output, a P2WSH output + // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want + // to calculate the contributed weight, so we use an all-zero hash. + .saturating_add(get_output_weight(&ScriptBuf::new_p2wsh( + &WScriptHash::from_raw_hash(Hash::all_zeros()) + )).to_wu()); + + // The splice initiator pays for the input spending the previous funding output. + if is_splice { + weight = weight + .saturating_add(BASE_INPUT_WEIGHT) + .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT) + .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); + #[cfg(feature = "grind_signatures")] + { + // Guarantees a low R signature + weight -= 1; + } } } + Weight::from_wu(weight) * feerate +} + +/// The components of a splice's funding transaction that are contributed by one party. +#[derive(Debug, Clone)] +pub struct FundingContribution { + value_added: Amount, + + value_removed: Amount, + + estimated_fee: Amount, + + /// The inputs included in the splice's funding transaction to meet the contributed amount + /// plus fees. Any excess amount will be sent to a change output. + inputs: Vec, + + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs plus fees will be the amount that is removed. + outputs: Vec, + + change_script: Option, + + feerate: FeeRate, + + is_initiator: bool, + + is_splice: bool, +} + +impl FundingContribution { + /// The net value contributed to a channel by the splice. If negative, more value will be + /// spliced out than spliced in. + pub fn net_value(&self) -> SignedAmount { + let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX); + let value_removed = self.value_removed.to_signed().unwrap_or(SignedAmount::MAX); + + value_added - value_removed + } + + pub(super) fn feerate(&self) -> FeeRate { + self.feerate + } + + pub(super) fn is_initiator(&self) -> bool { + self.is_initiator + } + pub(super) fn into_tx_parts(self) -> (Vec, Vec, Option) { - match self { - SpliceContribution::SpliceIn { inputs, change_script, .. } => { - (inputs, vec![], change_script) - }, - SpliceContribution::SpliceOut { outputs } => (vec![], outputs, None), + let FundingContribution { inputs, outputs, change_script, .. } = self; + (inputs, outputs, change_script) + } + + pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec, Vec) { + ( + self.inputs.into_iter().map(|input| input.utxo.outpoint).collect(), + self.outputs, + ) + } + + pub(super) fn validate(&self) -> Result { + for FundingTxInput { utxo, prevtx, .. } in self.inputs.iter() { + use crate::util::ser::Writeable; + const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { + channel_id: ChannelId([0; 32]), + serial_id: 0, + prevtx: None, + prevtx_out: 0, + sequence: 0, + // Mutually exclusive with prevtx, which is accounted for below. + shared_input_txid: None, + }; + let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); + if message_len > LN_MAX_MSG_LEN { + return Err(format!("Funding input references a prevtx that is too large for tx_add_input: {}", utxo.outpoint)); + } + } + + // Fees for splice-out are paid from the channel balance whereas fees for splice-in + // are paid by the funding inputs. Therefore, in the case of splice-out, we add the + // fees on top of the user-specified contribution. We leave the user-specified + // contribution as-is for splice-ins. + if !self.inputs.is_empty() { + let mut total_input_value = Amount::ZERO; + for FundingTxInput { utxo, .. } in self.inputs.iter() { + total_input_value = total_input_value + .checked_add(utxo.output.value) + .ok_or("Sum of input values is greater than the total bitcoin supply")?; + } + + // If the inputs are enough to cover intended contribution amount, with fees even when + // there is a change output, we are fine. + // If the inputs are less, but enough to cover intended contribution amount, with + // (lower) fees with no change, we are also fine (change will not be generated). + // So it's enough to check considering the lower, no-change fees. + // + // Note: dust limit is not relevant in this check. + // + // TODO(splicing): refine check including the fact wether a change will be added or not. + // Can be done once dual funding preparation is included. + + let contributed_input_value = self.value_added; + let estimated_fee = self.estimated_fee; + let minimal_input_amount_needed = contributed_input_value + .checked_add(estimated_fee) + .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?; + if total_input_value < minimal_input_amount_needed { + return Err(format!( + "Total input amount {total_input_value} is lower than needed for splice-in contribution {contributed_input_value}, considering fees of {estimated_fee}. Need more inputs.", + )); + } } + + let unpaid_fees = self + .inputs + .is_empty() + .then_some(self.estimated_fee) + .unwrap_or(Amount::ZERO) + .to_signed() + .expect("fees should never exceed Amount::MAX_MONEY"); + let contribution_amount = self.net_value(); + let adjusted_contribution = contribution_amount + .checked_sub(unpaid_fees) + .ok_or(format!( + "{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", + contribution_amount.unsigned_abs(), + self.estimated_fee, + ))?; + + Ok(adjusted_contribution) } } @@ -90,26 +396,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), }); @@ -127,8 +429,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, }) } @@ -217,22 +519,297 @@ 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 bitcoin::{Amount, FeeRate, ScriptBuf, WPubkeyHash}; + use bitcoin::hashes::Hash; + use bitcoin::transaction::{Transaction, TxOut, Version}; + use super::{FundingContribution, FundingTxInput, estimate_transaction_fee}; + + #[test] + #[rustfmt::skip] + fn test_estimate_transaction_fee() { + let one_input = [funding_input_sats(1_000)]; + let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)]; + + // 2 inputs, initiator, 2000 sat/kw feerate + assert_eq!( + estimate_transaction_fee(&two_inputs, &[], true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }), + ); + + // higher feerate + assert_eq!( + estimate_transaction_fee(&two_inputs, &[], true, false, FeeRate::from_sat_per_kwu(3000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }), + ); + + // only 1 input + assert_eq!( + estimate_transaction_fee(&one_input, &[], true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 970 } else { 972 }), + ); + + // 0 inputs + assert_eq!( + estimate_transaction_fee(&[], &[], true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(428), + ); + + // not initiator + assert_eq!( + estimate_transaction_fee(&[], &[], false, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(0), + ); + + // splice initiator + assert_eq!( + estimate_transaction_fee(&one_input, &[], true, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }), + ); + + // splice acceptor + assert_eq!( + estimate_transaction_fee(&one_input, &[], false, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 542 } else { 544 }), + ); + } + + #[rustfmt::skip] + fn funding_input_sats(input_value_sats: u64) -> FundingTxInput { + let prevout = TxOut { + value: Amount::from_sat(input_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }; + let prevtx = Transaction { + input: vec![], output: vec![prevout], + version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, + }; + + FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() + } + + fn funding_output_sats(output_value_sats: u64) -> TxOut { + TxOut { + value: Amount::from_sat(output_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + } + } + + #[test] + #[rustfmt::skip] + fn test_check_v2_funding_inputs_sufficient() { + // 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![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); + } + + // Net splice-in + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![ + funding_output_sats(200_000), + ], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); + } + + // Net splice-out + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![ + funding_output_sats(400_000), + ], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); + } + + // Net splice-out, inputs insufficient to cover fees + { + let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![ + funding_output_sats(400_000), + ], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(90000), + }; + assert_eq!( + 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![], + change_script: None, + 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![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); + } + + // higher fee rate, does not cover + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(298032), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2200), + }; + assert_eq!( + 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![], + change_script: None, + is_initiator: false, + is_splice: false, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); + } + } } diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 4340aad420a..17f827adf40 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -2061,9 +2061,13 @@ impl InteractiveTxConstructor { let mut inputs_to_contribute: Vec<(SerialId, InputOwned)> = inputs_to_contribute .into_iter() - .map(|FundingTxInput { utxo, sequence, prevtx: prev_tx }| { + .map(|FundingTxInput { utxo, prevtx: prev_tx }| { let serial_id = generate_holder_serial_id(entropy_source, is_initiator); - let txin = TxIn { previous_output: utxo.outpoint, sequence, ..Default::default() }; + let txin = TxIn { + previous_output: utxo.outpoint, + sequence: utxo.sequence, + ..Default::default() + }; let prev_output = utxo.output; let input = InputOwned::Single(SingleOwnedInput { input: txin, @@ -2337,22 +2341,21 @@ impl InteractiveTxConstructor { pub(super) fn calculate_change_output_value( context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, change_output_dust_limit: u64, -) -> Result, AbortReason> { - assert!(context.our_funding_contribution > SignedAmount::ZERO); - let our_funding_contribution_satoshis = context.our_funding_contribution.to_sat() as u64; - - let mut total_input_satoshis = 0u64; +) -> Result, AbortReason> { + let mut total_input_value = Amount::ZERO; let mut our_funding_inputs_weight = 0u64; for FundingTxInput { utxo, .. } in context.our_funding_inputs.iter() { - total_input_satoshis = total_input_satoshis.saturating_add(utxo.output.value.to_sat()); + total_input_value = total_input_value.checked_add(utxo.output.value).unwrap_or(Amount::MAX); let weight = BASE_INPUT_WEIGHT + utxo.satisfaction_weight; our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight); } let funding_outputs = &context.our_funding_outputs; - let total_output_satoshis = - funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat())); + let total_output_value = funding_outputs + .iter() + .fold(Amount::ZERO, |total, out| total.checked_add(out.value).unwrap_or(Amount::MAX)); + let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu()) }); @@ -2376,15 +2379,25 @@ pub(super) fn calculate_change_output_value( } } - let fees_sats = fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight); - let net_total_less_fees = - total_input_satoshis.saturating_sub(total_output_satoshis).saturating_sub(fees_sats); - if net_total_less_fees < our_funding_contribution_satoshis { + let contributed_fees = + Amount::from_sat(fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight)); + + let contributed_input_value = + context.our_funding_contribution + total_output_value.to_signed().unwrap(); + assert!(contributed_input_value > SignedAmount::ZERO); + let contributed_input_value = contributed_input_value.unsigned_abs(); + + let total_input_value_less_fees = + total_input_value.checked_sub(contributed_fees).unwrap_or(Amount::ZERO); + if total_input_value_less_fees < contributed_input_value { // Not enough to cover contribution plus fees return Err(AbortReason::InsufficientFees); } - let remaining_value = net_total_less_fees.saturating_sub(our_funding_contribution_satoshis); - if remaining_value < change_output_dust_limit { + + let remaining_value = total_input_value_less_fees + .checked_sub(contributed_input_value) + .expect("remaining_value should not be negative"); + if remaining_value.to_sat() < change_output_dust_limit { // Enough to cover contribution plus fees, but leftover is below dust limit; no change Ok(None) } else { @@ -3440,14 +3453,14 @@ mod tests { total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some((gross_change - fees - common_fees).to_sat())), + Ok(Some(gross_change - fees - common_fees)), ); // There is leftover for change, without common fees let context = FundingNegotiationContext { is_initiator: false, ..context }; assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some((gross_change - fees).to_sat())), + Ok(Some(gross_change - fees)), ); // Insufficient inputs, no leftover @@ -3482,7 +3495,7 @@ mod tests { total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 100), - Ok(Some((gross_change - fees).to_sat())), + Ok(Some(gross_change - fees)), ); // Larger fee, smaller change @@ -3496,7 +3509,7 @@ mod tests { total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some((gross_change - fees * 3 - common_fees * 3).to_sat())), + Ok(Some(gross_change - fees * 3 - common_fees * 3)), ); } diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index a96af7bbc5d..5a755b7ea62 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -13,13 +13,13 @@ use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::chain::transaction::OutPoint; use crate::chain::ChannelMonitorUpdateStatus; -use crate::events::bump_transaction::sync::WalletSourceSync; +use crate::events::bump_transaction::sync::{WalletSourceSync, WalletSync}; use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType}; use crate::ln::chan_utils; use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT}; use crate::ln::functional_test_utils::*; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::SpliceContribution; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::types::ChannelId; use crate::routing::router::{PaymentParameters, RouteParameters}; @@ -27,8 +27,9 @@ use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_channel_signer::SignerOp; +use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash}; #[test] fn test_v1_splice_in_negative_insufficient_inputs() { @@ -37,37 +38,42 @@ fn test_v1_splice_in_negative_insufficient_inputs() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); // Amount being added to the channel through the splice-in - let splice_in_sats = 20_000; + let splice_in_value = Amount::from_sat(20_000); // Create additional inputs, but insufficient - let extra_splice_funding_input_sats = splice_in_sats - 1; - let funding_inputs = - create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); - - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_sats), - inputs: funding_inputs, - change_script: None, - }; + let extra_splice_funding_input = splice_in_value - Amount::ONE_SAT; + + let coinbase_tx = nodes[0].provide_funding_utxos(1, extra_splice_funding_input); + let contribution = SpliceContribution::splice_in(splice_in_value, None); // Initiate splice-in, with insufficient input contribution - let res = nodes[0].node.splice_channel( + nodes[0].node.splice_channel( &channel_id, &nodes[1].node.get_our_node_id(), contribution, - 1024, // funding_feerate_per_kw, - None, // locktime - ); - match res { - Err(APIError::APIMisuseError { err }) => { - assert!(err.contains("Need more inputs")) - }, - _ => panic!("Wrong error {:?}", res.err().unwrap()), - } + ).unwrap(); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let event = get_event!(nodes[0], Event::FundingNeeded); + let funding_template = match event { + Event::FundingNeeded { funding_template, .. } => funding_template, + _ => panic!("Expected Event::FundingNeeded"), + }; + + let wallet = WalletSync::new(nodes[0].wallet_source.clone(), nodes[0].logger); + let feerate = FeeRate::from_sat_per_kwu(1024); + assert!(funding_template.build_sync(&wallet, feerate).is_err()); } pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>( @@ -98,8 +104,6 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( &channel_id, &node_id_acceptor, initiator_contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -108,6 +112,8 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + fund_splice(initiator, acceptor, channel_id); + let splice_init = get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); acceptor.node.handle_splice_init(node_id_initiator, &splice_init); let splice_ack = get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); @@ -122,6 +128,24 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( new_funding_script } +pub fn fund_splice<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, +) { + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let event = get_event!(initiator, Event::FundingNeeded); + let funding_template = match event { + Event::FundingNeeded { funding_template, .. } => funding_template, + _ => panic!("Expected Event::FundingNeeded"), + }; + + let wallet = WalletSync::new(initiator.wallet_source.clone(), initiator.logger); + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_contribution = funding_template.build_sync(&wallet, feerate).unwrap(); + let locktime = None; + initiator.node.funding_contributed(&channel_id, &node_id_acceptor, funding_contribution, locktime).unwrap(); +} + pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, initiator_contribution: SpliceContribution, new_funding_script: ScriptBuf, @@ -138,8 +162,8 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( }) .map(|channel| channel.funding_txo.unwrap()) .unwrap(); - let (initiator_inputs, initiator_outputs, initiator_change_script) = - initiator_contribution.into_tx_parts(); + let initiator_inputs = initiator.wallet_source.utxos(); + let (initiator_outputs, initiator_change_script) = initiator_contribution.into_tx_parts(); let mut expected_initiator_inputs = initiator_inputs .iter() .map(|input| input.utxo.outpoint) @@ -425,20 +449,16 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); nodes[0] .node .splice_channel( &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -450,6 +470,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); let _ = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); @@ -491,8 +513,6 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -504,6 +524,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); @@ -550,8 +572,6 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -564,6 +584,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); @@ -683,20 +705,16 @@ fn test_config_reject_inbound_splices() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); nodes[0] .node .splice_channel( &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -705,6 +723,8 @@ fn test_config_reject_inbound_splices() { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); @@ -744,18 +764,26 @@ 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 initiator_contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(initial_channel_value_sat * 2), - inputs: vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let utxo_value = added_value * 3 / 4; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let fees = Amount::from_sat(321); + + let coinbase_tx = nodes[0].provide_funding_utxos(2, utxo_value); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); + + let initiator_contribution = SpliceContribution::splice_in( + added_value, + Some(change_script.clone()), + ); let splice_tx = 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, + expected_change, + ); + mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); @@ -785,32 +813,184 @@ fn test_splice_out() { let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - let initiator_contribution = SpliceContribution::SpliceOut { - outputs: vec![ + let initiator_contribution = SpliceContribution::splice_out(vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]); + + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat < initial_channel_value_sat / 2 * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat < initial_channel_value_sat / 2 * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); +} + +#[test] +fn test_splice_in_and_out() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); + + // Contribute a net negative value, with fees taken from the contributed inputs and the + // remaining value sent to change + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + let added_value = Amount::from_sat(htlc_limit_msat / 1000); + let removed_value = added_value * 2; + let utxo_value = added_value * 3 / 4; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let fees = if cfg!(feature = "grind_signatures") { + Amount::from_sat(383) + } else { + Amount::from_sat(384) + }; + + assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); + + let coinbase_tx = nodes[0].provide_funding_utxos(2, utxo_value); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); + + let initiator_contribution = SpliceContribution::splice_in_and_out( + added_value, + vec![ TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), + value: removed_value / 2, script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }, TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), + value: removed_value / 2, script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), }, ], - }; + Some(change_script.clone()), + ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = utxo_value * 2 - added_value - fees; + assert_eq!( + splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + expected_change, + ); + mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat < initial_channel_value_sat / 2 * 1000); + assert!(htlc_limit_msat < added_value.to_sat() * 1000); let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat < initial_channel_value_sat / 2 * 1000); + assert!(htlc_limit_msat < added_value.to_sat() * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + nodes[0].wallet_source.clear_utxos(); + + // Contribute a net positive value, with fees taken from the contributed inputs and the + // remaining value sent to change + let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let removed_value = added_value / 2; + let utxo_value = added_value * 3 / 4; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let fees = if cfg!(feature = "grind_signatures") { + Amount::from_sat(383) + } else { + Amount::from_sat(384) + }; + + let coinbase_tx = nodes[0].provide_funding_utxos(2, utxo_value); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); + + let initiator_contribution = SpliceContribution::splice_in_and_out( + added_value, + vec![ + TxOut { + value: removed_value / 2, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: removed_value / 2, + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ], + Some(change_script.clone()), + ); + + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = utxo_value * 2 - added_value - fees; + assert_eq!( + splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + expected_change, + ); + + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert_eq!(htlc_limit_msat, 0); + + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + nodes[0].wallet_source.clear_utxos(); + + // Fail adding a net contribution value of zero + let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let removed_value = added_value; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + + let coinbase_tx = nodes[0].provide_funding_utxos(2, Amount::ONE_BTC); + let initiator_contribution = SpliceContribution::splice_in_and_out( + added_value, + vec![ + TxOut { + value: removed_value / 2, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: removed_value / 2, + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ], + Some(change_script), + ); + + assert_eq!( + nodes[0].node.splice_channel( + &channel_id, + &nodes[1].node.get_our_node_id(), + initiator_contribution, + ), + Err(APIError::APIMisuseError { + err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id), + }), + ); } #[cfg(test)] @@ -847,18 +1027,19 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: let (_, _, channel_id, initial_funding_tx) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = nodes[0].provide_funding_utxos(1, Amount::ONE_BTC); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); // We want to have two HTLCs pending to make sure we can claim those sent before and after a // splice negotiation. let payment_amount = 1_000_000; let (preimage1, payment_hash1, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); + let splice_in_amount = initial_channel_capacity / 2; - let initiator_contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let initiator_contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); let (preimage2, payment_hash2, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); let htlc_expiry = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS; @@ -1052,18 +1233,16 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { route_payment(&nodes[0], &[&nodes[1]], 1_000_000); // Negotiate the splice up until the nodes exchange `tx_complete`. - let initiator_contribution = SpliceContribution::SpliceOut { - outputs: vec![ - TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }, - TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }, - ], - }; + let initiator_contribution = SpliceContribution::splice_out(vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]); let initial_commit_sig_for_acceptor = negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); assert_eq!(initial_commit_sig_for_acceptor.htlc_signatures.len(), 1); @@ -1340,38 +1519,30 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { nodes[1].node.peer_disconnected(node_id_0); let splice_out_sat = initial_channel_value_sat / 4; - let node_0_contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(splice_out_sat), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let node_0_contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); nodes[0] .node .splice_channel( &channel_id, &node_id_1, node_0_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); - let node_1_contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(splice_out_sat), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }], - }; + let node_1_contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]); nodes[1] .node .splice_channel( &channel_id, &node_id_0, node_1_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -1406,6 +1577,9 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { } reconnect_args.send_stfu = (true, true); reconnect_nodes(reconnect_args); + + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -1558,6 +1732,8 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { panic!("Unexpected event {:?}", &msg_events[0]); } + fund_splice(&nodes[1], &nodes[0], channel_id); + let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); let initial_commit_sig = { nodes[0].node.handle_splice_init(node_id_1, &splice_init); @@ -1614,13 +1790,12 @@ fn disconnect_on_unexpected_interactive_tx_message() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Complete interactive-tx construction, but fail by having the acceptor send a duplicate // tx_complete instead of commitment_signed. @@ -1654,13 +1829,12 @@ fn fail_splice_on_interactive_tx_error() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Fail during interactive-tx construction by having the acceptor echo back tx_add_input instead // of sending tx_complete. The failure occurs because the serial id will have the wrong parity. @@ -1678,7 +1852,7 @@ fn fail_splice_on_interactive_tx_error() { match event { Event::SpliceFailed { contributed_inputs, .. } => { assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); + assert_eq!(contributed_inputs[0], nodes[0].wallet_source.utxos()[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1730,7 +1904,7 @@ fn fail_splice_on_interactive_tx_error() { match event { Event::SpliceFailed { contributed_inputs, .. } => { assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); + assert_eq!(contributed_inputs[0], initiator.wallet_source.utxos()[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1760,13 +1934,12 @@ fn fail_splice_on_tx_abort() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Fail during interactive-tx construction by having the acceptor send tx_abort instead of // tx_complete. @@ -1787,7 +1960,7 @@ fn fail_splice_on_tx_abort() { match event { Event::SpliceFailed { contributed_inputs, .. } => { assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); + assert_eq!(contributed_inputs[0], initiator.wallet_source.utxos()[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1814,13 +1987,12 @@ fn fail_splice_on_channel_close() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Close the channel before completion of interactive-tx construction. let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); @@ -1865,13 +2037,12 @@ 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::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + //vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Close the channel before completion of STFU handshake. initiator @@ -1880,8 +2051,6 @@ fn fail_quiescent_action_on_channel_close() { &channel_id, &node_id_acceptor, contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -1960,23 +2129,19 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw // Splice both channels, lock them, and connect enough blocks to trigger the legacy SCID pruning // logic while the HTLC is still pending. - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); let splice_tx_0_1 = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution); for node in &nodes { mine_transaction(node, &splice_tx_0_1); } - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]); let splice_tx_1_2 = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution); for node in &nodes { mine_transaction(node, &splice_tx_1_2); 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 34c6414b7e0..d78162b1ab4 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; @@ -2210,7 +2210,7 @@ impl Drop for TestScorer { pub struct TestWalletSource { secret_key: SecretKey, - utxos: Mutex>, + utxos: Mutex>, secp: Secp256k1, } @@ -2219,39 +2219,35 @@ 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) { self.utxos.lock().unwrap().clear(); } + pub fn utxos(&self) -> Vec { + self.utxos.lock().unwrap().clone() + } + 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) + 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"))] @@ -2276,7 +2272,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 {