diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ced89f5ac8c..0abeea1c828 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -46,7 +46,6 @@ use lightning::chain::{ chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch, }; use lightning::events; -use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync}; use lightning::ln::channel::{ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS, }; @@ -81,6 +80,7 @@ use lightning::util::logger::Logger; use lightning::util::ser::{LengthReadable, ReadableArgs, Writeable, Writer}; use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner}; use lightning::util::test_utils::TestWalletSource; +use lightning::util::wallet_utils::{WalletSourceSync, WalletSync}; use lightning_invoice::RawBolt11Invoice; diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 6adb8f33c89..085165e9e02 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -40,7 +40,6 @@ use lightning::chain::chaininterface::{ use lightning::chain::chainmonitor; use lightning::chain::transaction::OutPoint; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; -use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync}; use lightning::events::Event; use lightning::ln::channel_state::ChannelDetails; use lightning::ln::channelmanager::{ChainParameters, ChannelManager, InterceptId, PaymentId}; @@ -71,6 +70,7 @@ use lightning::util::logger::Logger; use lightning::util::ser::{Readable, Writeable}; use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner}; use lightning::util::test_utils::TestWalletSource; +use lightning::util::wallet_utils::{WalletSourceSync, WalletSync}; use lightning_invoice::RawBolt11Invoice; diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs index f18e0e56800..93d671b176d 100644 --- a/lightning-tests/src/upgrade_downgrade_tests.rs +++ b/lightning-tests/src/upgrade_downgrade_tests.rs @@ -46,7 +46,6 @@ use lightning_0_0_125::routing::router as router_0_0_125; use lightning_0_0_125::util::ser::Writeable as _; use lightning::chain::channelmonitor::{ANTI_REORG_DELAY, HTLC_FAIL_BACK_BUFFER}; -use lightning::events::bump_transaction::sync::WalletSourceSync; use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType}; use lightning::ln::functional_test_utils::*; use lightning::ln::msgs::BaseMessageHandler as _; @@ -55,6 +54,7 @@ use lightning::ln::msgs::MessageSendEvent; use lightning::ln::splicing_tests::*; use lightning::ln::types::ChannelId; use lightning::sign::OutputSpender; +use lightning::util::wallet_utils::WalletSourceSync; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 37351460634..925bdfb62f8 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -4039,9 +4039,14 @@ impl ChannelMonitorImpl { } if let Some(parent_funding_txid) = channel_parameters.splice_parent_funding_txid.as_ref() { - // Only one splice can be negotiated at a time after we've exchanged `channel_ready` - // (implying our funding is confirmed) that spends our currently locked funding. - if !self.pending_funding.is_empty() { + // Multiple RBF candidates for the same splice are allowed (they share the same + // parent funding txid). A new splice with a different parent while one is pending + // is not allowed. + let has_different_parent = self.pending_funding.iter().any(|funding| { + funding.channel_parameters.splice_parent_funding_txid.as_ref() + != Some(parent_funding_txid) + }); + if has_different_parent { log_error!( logger, "Negotiated splice while channel is pending channel_ready/splice_locked" diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index c783e96381a..6a5e9948653 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -14,47 +14,33 @@ pub mod sync; use alloc::collections::BTreeMap; -use core::future::Future; -use core::ops::Deref; use crate::chain::chaininterface::{ compute_feerate_sat_per_1000_weight, fee_for_weight, BroadcasterInterface, TransactionType, }; use crate::chain::ClaimId; -use crate::io_extras::sink; use crate::ln::chan_utils; use crate::ln::chan_utils::{ shared_anchor_script_pubkey, HTLCOutputInCommitment, ANCHOR_INPUT_WITNESS_WEIGHT, - BASE_INPUT_WEIGHT, BASE_TX_SIZE, EMPTY_SCRIPT_SIG_WEIGHT, EMPTY_WITNESS_WEIGHT, - HTLC_SUCCESS_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, - 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, + EMPTY_SCRIPT_SIG_WEIGHT, EMPTY_WITNESS_WEIGHT, HTLC_SUCCESS_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, + HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, + HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_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; -use crate::sign::{ - ChannelDerivationParameters, HTLCDescriptor, SignerProvider, P2TR_KEY_PATH_WITNESS_WEIGHT, - P2WPKH_WITNESS_WEIGHT, -}; -use crate::sync::Mutex; -use crate::util::async_poll::{MaybeSend, MaybeSync}; +use crate::sign::{ChannelDerivationParameters, HTLCDescriptor, SignerProvider}; use crate::util::logger::Logger; +use crate::util::wallet_utils::{CoinSelection, CoinSelectionSource, ConfirmedUtxo, Input}; use bitcoin::amount::Amount; -use bitcoin::consensus::Encodable; -use bitcoin::constants::WITNESS_SCALE_FACTOR; -use bitcoin::key::TweakedPublicKey; use bitcoin::locktime::absolute::LockTime; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; use bitcoin::secp256k1; use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::transaction::Version; -use bitcoin::{ - OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, -}; +use bitcoin::{OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; /// A descriptor used to sign for a commitment transaction's anchor output. #[derive(Clone, Debug, PartialEq, Eq)] @@ -258,499 +244,6 @@ pub enum BumpTransactionEvent { }, } -/// An input that must be included in a transaction when performing coin selection through -/// [`CoinSelectionSource::select_confirmed_utxos`]. It is guaranteed to be a SegWit input, so it -/// must have an empty [`TxIn::script_sig`] when spent. -#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] -pub struct Input { - /// The unique identifier of the input. - pub outpoint: OutPoint, - /// The UTXO being spent by the input. - pub previous_utxo: TxOut, - /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and - /// [`TxIn::witness`], each with their lengths included, required to satisfy the output's - /// script. - pub satisfaction_weight: u64, -} - -/// An unspent transaction output that is available to spend resulting from a successful -/// [`CoinSelection`] attempt. -#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] -pub struct Utxo { - /// The unique identifier of the output. - pub outpoint: OutPoint, - /// The output to spend. - pub output: TxOut, - /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and [`TxIn::witness`], each - /// 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 { - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a legacy P2PKH output. - pub fn new_p2pkh(outpoint: OutPoint, value: Amount, pubkey_hash: &PubkeyHash) -> Self { - let script_sig_size = 1 /* script_sig length */ + - 1 /* OP_PUSH73 */ + - 73 /* sig including sighash flag */ + - 1 /* OP_PUSH33 */ + - 33 /* pubkey */; - Self { - 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, - } - } - - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a P2WPKH nested in P2SH output. - pub fn new_nested_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { - let script_sig_size = 1 /* script_sig length */ + - 1 /* OP_0 */ + - 1 /* OP_PUSH20 */ + - 20 /* pubkey_hash */; - Self { - outpoint, - output: TxOut { - value, - script_pubkey: ScriptBuf::new_p2sh( - &ScriptBuf::new_p2wpkh(pubkey_hash).script_hash(), - ), - }, - satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 - + P2WPKH_WITNESS_WEIGHT, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - } - } - - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a SegWit v0 P2WPKH output. - pub fn new_v0_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { - Self { - 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, - } - } - - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a keypath spend of a SegWit v1 P2TR output. - pub fn new_v1_p2tr( - outpoint: OutPoint, value: Amount, tweaked_public_key: TweakedPublicKey, - ) -> Self { - Self { - outpoint, - output: TxOut { value, script_pubkey: ScriptBuf::new_p2tr_tweaked(tweaked_public_key) }, - satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2TR_KEY_PATH_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, - /// 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 - /// not met. This implies no other party should be able to spend it except us. - pub change_output: Option, -} - -impl CoinSelection { - fn satisfaction_weight(&self) -> u64 { - self.confirmed_utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.satisfaction_weight).sum() - } - - fn input_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`], -/// which can provide a default implementation of this trait when used with [`Wallet`]. -/// -/// For a synchronous version of this trait, see [`sync::CoinSelectionSourceSync`]. -/// -/// This is not exported to bindings users as async is only supported in Rust. -// Note that updates to documentation on this trait should be copied to the synchronous version. -pub trait CoinSelectionSource { - /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are - /// available to spend. Implementations are free to pick their coin selection algorithm of - /// choice, as long as the following requirements are met: - /// - /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction - /// throughout coin selection, but must not be returned as part of the result. - /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction - /// throughout coin selection. In some cases, like when funding an anchor transaction, this - /// set is empty. Implementations should ensure they handle this correctly on their end, - /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be - /// provided, in which case a zero-value empty OP_RETURN output can be used instead. - /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the - /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. - /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this - /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC - /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for - /// anchor transactions, we will try your coin selection again with the same input-output - /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions - /// cannot be downsized. - /// - /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of - /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require - /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and - /// delaying block inclusion. - /// - /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they - /// can be re-used within new fee-bumped iterations of the original claiming transaction, - /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a - /// transaction associated with it, and all of the available UTXOs have already been assigned to - /// other claims, implementations must be willing to double spend their UTXOs. The choice of - /// which UTXOs to double spend is left to the implementation, but it must strive to keep the - /// set of other claims being double spent to a minimum. - /// - /// If `claim_id` is not set, then the selection should be treated as if it were for a unique - /// claim and must NOT be double-spent rather than being kept to a minimum. - /// - /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims - fn select_confirmed_utxos<'a>( - &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> impl Future> + MaybeSend + 'a; - /// Signs and provides the full witness for all inputs within the transaction known to the - /// trait (i.e., any provided via [`CoinSelectionSource::select_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future> + MaybeSend + 'a; -} - -/// An alternative to [`CoinSelectionSource`] that can be implemented and used along [`Wallet`] to -/// provide a default implementation to [`CoinSelectionSource`]. -/// -/// For a synchronous version of this trait, see [`sync::WalletSourceSync`]. -/// -/// This is not exported to bindings users as async is only supported in Rust. -// Note that updates to documentation on this trait should be copied to the synchronous version. -pub trait WalletSource { - /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. - fn list_confirmed_utxos<'a>( - &'a self, - ) -> impl Future, ()>> + MaybeSend + 'a; - - /// Returns the previous transaction containing the UTXO referenced by the outpoint. - fn get_prevtx<'a>( - &'a self, outpoint: OutPoint, - ) -> impl Future> + MaybeSend + 'a; - - /// Returns a script to use for change above dust resulting from a successful coin selection - /// attempt. - fn get_change_script<'a>( - &'a self, - ) -> impl Future> + MaybeSend + 'a; - - /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within - /// the transaction known to the wallet (i.e., any provided via - /// [`WalletSource::list_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future> + MaybeSend + 'a; -} - -/// A wrapper over [`WalletSource`] that implements [`CoinSelectionSource`] by preferring UTXOs -/// that would avoid conflicting double spends. If not enough UTXOs are available to do so, -/// conflicting double spends may happen. -/// -/// For a synchronous version of this wrapper, see [`sync::WalletSync`]. -/// -/// This is not exported to bindings users as async is only supported in Rust. -// Note that updates to documentation on this struct should be copied to the synchronous version. -pub struct Wallet -where - W::Target: WalletSource + MaybeSend, -{ - source: W, - logger: L, - // TODO: Do we care about cleaning this up once the UTXOs have a confirmed spend? We can do so - // by checking whether any UTXOs that exist in the map are no longer returned in - // `list_confirmed_utxos`. - locked_utxos: Mutex>>, -} - -impl Wallet -where - W::Target: WalletSource + MaybeSend, -{ - /// Returns a new instance backed by the given [`WalletSource`] that serves as an implementation - /// of [`CoinSelectionSource`]. - pub fn new(source: W, logger: L) -> Self { - Self { source, logger, locked_utxos: Mutex::new(new_hash_map()) } - } - - /// Performs coin selection on the set of UTXOs obtained from - /// [`WalletSource::list_confirmed_utxos`]. Its algorithm can be described as "smallest - /// above-dust-after-spend first", with a slight twist: we may skip UTXOs that are above dust at - /// the target feerate after having spent them in a separate claim transaction if - /// `force_conflicting_utxo_spend` is unset to avoid producing conflicting transactions. If - /// `tolerate_high_network_feerates` is set, we'll attempt to spend UTXOs that contribute at - /// least 1 satoshi at the current feerate, otherwise, we'll only attempt to spend those which - /// contribute at least twice their fee. - async fn select_confirmed_utxos_internal( - &self, utxos: &[Utxo], claim_id: Option, force_conflicting_utxo_spend: bool, - tolerate_high_network_feerates: bool, target_feerate_sat_per_1000_weight: u32, - preexisting_tx_weight: u64, input_amount_sat: Amount, target_amount_sat: Amount, - max_tx_weight: u64, - ) -> Result { - debug_assert!(!(claim_id.is_none() && force_conflicting_utxo_spend)); - - // P2WSH and P2TR outputs are both the heaviest-weight standard outputs at 34 bytes - let max_coin_selection_weight = max_tx_weight - .checked_sub(preexisting_tx_weight + P2WSH_TXOUT_WEIGHT) - .ok_or_else(|| { - log_debug!( - self.logger, - "max_tx_weight is too small to accommodate the preexisting tx weight plus a P2WSH/P2TR output" - ); - })?; - - let mut selected_amount; - let mut total_fees; - let mut selected_utxos; - { - let mut locked_utxos = self.locked_utxos.lock().unwrap(); - let mut eligible_utxos = utxos - .iter() - .filter_map(|utxo| { - if let Some(utxo_claim_id) = locked_utxos.get(&utxo.outpoint) { - // TODO(splicing): For splicing (i.e., claim_id.is_none()), ideally we'd - // allow force_conflicting_utxo_spend for an RBF attempt. However, we'd need - // something similar to a ClaimId to identify a splice. - if (utxo_claim_id.is_none() || claim_id.is_none()) - || (*utxo_claim_id != claim_id && !force_conflicting_utxo_spend) - { - log_trace!( - self.logger, - "Skipping UTXO {} to prevent conflicting spend", - utxo.outpoint - ); - return None; - } - } - let fee_to_spend_utxo = Amount::from_sat(fee_for_weight( - target_feerate_sat_per_1000_weight, - BASE_INPUT_WEIGHT + utxo.satisfaction_weight, - )); - let should_spend = if tolerate_high_network_feerates { - utxo.output.value > fee_to_spend_utxo - } else { - utxo.output.value >= fee_to_spend_utxo * 2 - }; - if should_spend { - Some((utxo, fee_to_spend_utxo)) - } else { - log_trace!( - self.logger, - "Skipping UTXO {} due to dust proximity after spend", - utxo.outpoint - ); - None - } - }) - .collect::>(); - eligible_utxos.sort_unstable_by_key(|(utxo, fee_to_spend_utxo)| { - utxo.output.value - *fee_to_spend_utxo - }); - - selected_amount = input_amount_sat; - total_fees = Amount::from_sat(fee_for_weight( - target_feerate_sat_per_1000_weight, - preexisting_tx_weight, - )); - selected_utxos = VecDeque::new(); - // Invariant: `selected_utxos_weight` is never greater than `max_coin_selection_weight` - let mut selected_utxos_weight = 0; - for (utxo, fee_to_spend_utxo) in eligible_utxos { - if selected_amount >= target_amount_sat + total_fees { - break; - } - // First skip any UTXOs with prohibitive satisfaction weights - if BASE_INPUT_WEIGHT + utxo.satisfaction_weight > max_coin_selection_weight { - continue; - } - // If adding this UTXO to `selected_utxos` would push us over the - // `max_coin_selection_weight`, remove UTXOs from the front to make room - // for this new UTXO. - while selected_utxos_weight + BASE_INPUT_WEIGHT + utxo.satisfaction_weight - > max_coin_selection_weight - && !selected_utxos.is_empty() - { - let (smallest_value_after_spend_utxo, fee_to_spend_utxo): (Utxo, Amount) = - selected_utxos.pop_front().unwrap(); - selected_amount -= smallest_value_after_spend_utxo.output.value; - total_fees -= fee_to_spend_utxo; - selected_utxos_weight -= - BASE_INPUT_WEIGHT + smallest_value_after_spend_utxo.satisfaction_weight; - } - selected_amount += utxo.output.value; - total_fees += fee_to_spend_utxo; - selected_utxos_weight += BASE_INPUT_WEIGHT + utxo.satisfaction_weight; - selected_utxos.push_back((utxo.clone(), fee_to_spend_utxo)); - } - if selected_amount < target_amount_sat + total_fees { - log_debug!( - self.logger, - "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", - target_feerate_sat_per_1000_weight, - max_coin_selection_weight, - ); - return Err(()); - } - // Once we've selected enough UTXOs to cover `target_amount_sat + total_fees`, - // we may be able to remove some small-value ones while still covering - // `target_amount_sat + total_fees`. - while !selected_utxos.is_empty() - && selected_amount - selected_utxos.front().unwrap().0.output.value - >= target_amount_sat + total_fees - selected_utxos.front().unwrap().1 - { - let (smallest_value_after_spend_utxo, fee_to_spend_utxo) = - selected_utxos.pop_front().unwrap(); - selected_amount -= smallest_value_after_spend_utxo.output.value; - total_fees -= fee_to_spend_utxo; - } - for (utxo, _) in &selected_utxos { - locked_utxos.insert(utxo.outpoint, claim_id); - } - } - - let remaining_amount = selected_amount - target_amount_sat - total_fees; - let change_script = self.source.get_change_script().await?; - let change_output_fee = fee_for_weight( - target_feerate_sat_per_1000_weight, - (8 /* value */ + change_script.consensus_encode(&mut sink()).unwrap() as u64) - * WITNESS_SCALE_FACTOR as u64, - ); - let change_output_amount = - Amount::from_sat(remaining_amount.to_sat().saturating_sub(change_output_fee)); - let change_output = if change_output_amount < change_script.minimal_non_dust() { - log_debug!(self.logger, "Coin selection attempt did not yield change output"); - None - } else { - Some(TxOut { script_pubkey: change_script, value: change_output_amount }) - }; - - let mut confirmed_utxos = Vec::with_capacity(selected_utxos.len()); - for (utxo, _) in selected_utxos { - let prevtx = self.source.get_prevtx(utxo.outpoint).await?; - let prevtx_id = prevtx.compute_txid(); - if prevtx_id != utxo.outpoint.txid - || prevtx.output.get(utxo.outpoint.vout as usize).is_none() - { - log_error!( - self.logger, - "Tx {} from wallet source doesn't contain output referenced by outpoint: {}", - prevtx_id, - utxo.outpoint, - ); - return Err(()); - } - - confirmed_utxos.push(ConfirmedUtxo { utxo, prevtx }); - } - - Ok(CoinSelection { confirmed_utxos, change_output }) - } -} - -impl CoinSelectionSource - for Wallet -where - W::Target: WalletSource + MaybeSend + MaybeSync, -{ - fn select_confirmed_utxos<'a>( - &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> impl Future> + MaybeSend + 'a { - async move { - let utxos = self.source.list_confirmed_utxos().await?; - // TODO: Use fee estimation utils when we upgrade to bitcoin v0.30.0. - let total_output_size: u64 = must_pay_to - .iter() - .map( - |output| 8 /* value */ + 1 /* script len */ + output.script_pubkey.len() as u64, - ) - .sum(); - let total_satisfaction_weight: u64 = - must_spend.iter().map(|input| input.satisfaction_weight).sum(); - let total_input_weight = - (BASE_INPUT_WEIGHT * must_spend.len() as u64) + total_satisfaction_weight; - - let preexisting_tx_weight = SEGWIT_MARKER_FLAG_WEIGHT - + total_input_weight - + ((BASE_TX_SIZE + total_output_size) * WITNESS_SCALE_FACTOR as u64); - let input_amount_sat = must_spend.iter().map(|input| input.previous_utxo.value).sum(); - let target_amount_sat = must_pay_to.iter().map(|output| output.value).sum(); - - let configs = [(false, false), (false, true), (true, false), (true, true)]; - for (force_conflicting_utxo_spend, tolerate_high_network_feerates) in configs { - if claim_id.is_none() && force_conflicting_utxo_spend { - continue; - } - log_debug!( - self.logger, - "Attempting coin selection targeting {} sat/kW (force_conflicting_utxo_spend = {}, tolerate_high_network_feerates = {})", - target_feerate_sat_per_1000_weight, - force_conflicting_utxo_spend, - tolerate_high_network_feerates - ); - let attempt = self - .select_confirmed_utxos_internal( - &utxos, - claim_id, - force_conflicting_utxo_spend, - tolerate_high_network_feerates, - target_feerate_sat_per_1000_weight, - preexisting_tx_weight, - input_amount_sat, - target_amount_sat, - max_tx_weight, - ) - .await; - if attempt.is_ok() { - return attempt; - } - } - Err(()) - } - } - - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future> + MaybeSend + 'a { - self.source.sign_psbt(psbt) - } -} - /// A handler for [`Event::BumpTransaction`] events that sources confirmed UTXOs from a /// [`CoinSelectionSource`] to fee bump transactions via Child-Pays-For-Parent (CPFP) or /// Replace-By-Fee (RBF). @@ -763,12 +256,10 @@ where // Note that updates to documentation on this struct should be copied to the synchronous version. pub struct BumpTransactionEventHandler< B: BroadcasterInterface, - C: Deref, + C: CoinSelectionSource, SP: SignerProvider, L: Logger, -> where - C::Target: CoinSelectionSource, -{ +> { broadcaster: B, utxo_source: C, signer_provider: SP, @@ -776,10 +267,8 @@ pub struct BumpTransactionEventHandler< secp: Secp256k1, } -impl +impl BumpTransactionEventHandler -where - C::Target: CoinSelectionSource, { /// Returns a new instance capable of handling [`Event::BumpTransaction`] events. /// @@ -1349,18 +838,21 @@ where mod tests { use super::*; - use crate::events::bump_transaction::sync::{ - BumpTransactionEventHandlerSync, CoinSelectionSourceSync, - }; + use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync; use crate::io::Cursor; use crate::ln::chan_utils::ChannelTransactionParameters; use crate::ln::channel::ANCHOR_OUTPUT_VALUE_SATOSHI; use crate::sign::KeysManager; + use crate::sync::Mutex; use crate::types::features::ChannelTypeFeatures; use crate::util::ser::Readable; use crate::util::test_utils::{TestBroadcaster, TestLogger}; + use crate::util::wallet_utils::CoinSelectionSourceSync; + use crate::util::wallet_utils::Utxo; + use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::hex::FromHex; + use bitcoin::key::TweakedPublicKey; use bitcoin::{ Network, ScriptBuf, Transaction, WitnessProgram, WitnessVersion, XOnlyPublicKey, }; diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs index 39088bb0e97..f2e1be1590c 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -15,258 +15,12 @@ use core::pin::pin; use core::task; use crate::chain::chaininterface::BroadcasterInterface; -use crate::chain::ClaimId; -use crate::prelude::*; use crate::sign::SignerProvider; -use crate::util::async_poll::{dummy_waker, MaybeSend, MaybeSync}; +use crate::util::async_poll::dummy_waker; use crate::util::logger::Logger; +use crate::util::wallet_utils::{CoinSelectionSourceSync, CoinSelectionSourceSyncWrapper}; -use bitcoin::{OutPoint, Psbt, ScriptBuf, Transaction, TxOut}; - -use super::BumpTransactionEvent; -use super::{ - BumpTransactionEventHandler, CoinSelection, CoinSelectionSource, Input, Utxo, Wallet, - WalletSource, -}; - -/// An alternative to [`CoinSelectionSourceSync`] that can be implemented and used along -/// [`WalletSync`] to provide a default implementation to [`CoinSelectionSourceSync`]. -/// -/// For an asynchronous version of this trait, see [`WalletSource`]. -// Note that updates to documentation on this trait should be copied to the asynchronous version. -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 referenced by the outpoint. - fn get_prevtx(&self, outpoint: OutPoint) -> 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`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - /// - /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig - /// [`TxIn::witness`]: bitcoin::TxIn::witness - fn sign_psbt(&self, psbt: Psbt) -> Result; -} - -pub(crate) struct WalletSourceSyncWrapper(T) -where - T::Target: WalletSourceSync; - -// Implement `Deref` directly on WalletSourceSyncWrapper so that it can be used directly -// below, rather than via a wrapper. -impl Deref for WalletSourceSyncWrapper -where - T::Target: WalletSourceSync, -{ - type Target = Self; - fn deref(&self) -> &Self { - self - } -} - -impl WalletSource for WalletSourceSyncWrapper -where - T::Target: WalletSourceSync, -{ - fn list_confirmed_utxos<'a>( - &'a self, - ) -> impl Future, ()>> + MaybeSend + 'a { - let utxos = self.0.list_confirmed_utxos(); - async move { utxos } - } - - fn get_prevtx<'a>( - &'a self, outpoint: OutPoint, - ) -> impl Future> + MaybeSend + 'a { - let prevtx = self.0.get_prevtx(outpoint); - Box::pin(async move { prevtx }) - } - - fn get_change_script<'a>( - &'a self, - ) -> impl Future> + MaybeSend + 'a { - let script = self.0.get_change_script(); - async move { script } - } - - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future> + MaybeSend + 'a { - let signed_psbt = self.0.sign_psbt(psbt); - async move { signed_psbt } - } -} - -/// A wrapper over [`WalletSourceSync`] that implements [`CoinSelectionSourceSync`] by preferring -/// UTXOs that would avoid conflicting double spends. If not enough UTXOs are available to do so, -/// conflicting double spends may happen. -/// -/// For an asynchronous version of this wrapper, see [`Wallet`]. -// Note that updates to documentation on this struct should be copied to the asynchronous version. -pub struct WalletSync -where - W::Target: WalletSourceSync + MaybeSend, -{ - wallet: Wallet, L>, -} - -impl WalletSync -where - W::Target: WalletSourceSync + MaybeSend, -{ - /// Constructs a new [`WalletSync`] instance. - pub fn new(source: W, logger: L) -> Self { - Self { wallet: Wallet::new(WalletSourceSyncWrapper(source), logger) } - } -} - -impl CoinSelectionSourceSync - for WalletSync -where - W::Target: WalletSourceSync + MaybeSend + MaybeSync, -{ - fn select_confirmed_utxos( - &self, claim_id: Option, must_spend: Vec, must_pay_to: &[TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> Result { - let fut = self.wallet.select_confirmed_utxos( - claim_id, - must_spend, - must_pay_to, - target_feerate_sat_per_1000_weight, - max_tx_weight, - ); - let mut waker = dummy_waker(); - let mut ctx = task::Context::from_waker(&mut waker); - match pin!(fut).poll(&mut ctx) { - task::Poll::Ready(result) => result, - task::Poll::Pending => { - unreachable!( - "Wallet::select_confirmed_utxos should not be pending in a sync context" - ); - }, - } - } - - fn sign_psbt(&self, psbt: Psbt) -> Result { - let fut = self.wallet.sign_psbt(psbt); - let mut waker = dummy_waker(); - let mut ctx = task::Context::from_waker(&mut waker); - match pin!(fut).poll(&mut ctx) { - task::Poll::Ready(result) => result, - task::Poll::Pending => { - unreachable!("Wallet::sign_psbt should not be pending in a sync context"); - }, - } - } -} - -/// 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 -/// [`WalletSourceSync`], which can provide a default implementation of this trait when used with -/// [`WalletSync`]. -/// -/// For an asynchronous version of this trait, see [`CoinSelectionSource`]. -// Note that updates to documentation on this trait should be copied to the asynchronous version. -pub trait CoinSelectionSourceSync { - /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are - /// available to spend. Implementations are free to pick their coin selection algorithm of - /// choice, as long as the following requirements are met: - /// - /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction - /// throughout coin selection, but must not be returned as part of the result. - /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction - /// throughout coin selection. In some cases, like when funding an anchor transaction, this - /// set is empty. Implementations should ensure they handle this correctly on their end, - /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be - /// provided, in which case a zero-value empty OP_RETURN output can be used instead. - /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the - /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. - /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this - /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC - /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for - /// anchor transactions, we will try your coin selection again with the same input-output - /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions - /// cannot be downsized. - /// - /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of - /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require - /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and - /// delaying block inclusion. - /// - /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they - /// can be re-used within new fee-bumped iterations of the original claiming transaction, - /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a - /// transaction associated with it, and all of the available UTXOs have already been assigned to - /// other claims, implementations must be willing to double spend their UTXOs. The choice of - /// which UTXOs to double spend is left to the implementation, but it must strive to keep the - /// set of other claims being double spent to a minimum. - /// - /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims - fn select_confirmed_utxos( - &self, claim_id: Option, must_spend: Vec, must_pay_to: &[TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> Result; - - /// Signs and provides the full witness for all inputs within the transaction known to the - /// trait (i.e., any provided via [`CoinSelectionSourceSync::select_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - fn sign_psbt(&self, psbt: Psbt) -> Result; -} - -struct CoinSelectionSourceSyncWrapper(T) -where - T::Target: CoinSelectionSourceSync; - -// Implement `Deref` directly on CoinSelectionSourceSyncWrapper so that it can be used directly -// below, rather than via a wrapper. -impl Deref for CoinSelectionSourceSyncWrapper -where - T::Target: CoinSelectionSourceSync, -{ - type Target = Self; - fn deref(&self) -> &Self { - self - } -} - -impl CoinSelectionSource for CoinSelectionSourceSyncWrapper -where - T::Target: CoinSelectionSourceSync, -{ - fn select_confirmed_utxos<'a>( - &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> impl Future> + MaybeSend + 'a { - let coins = self.0.select_confirmed_utxos( - claim_id, - must_spend, - must_pay_to, - target_feerate_sat_per_1000_weight, - max_tx_weight, - ); - async move { coins } - } - - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future> + MaybeSend + 'a { - let psbt = self.0.sign_psbt(psbt); - async move { psbt } - } -} +use super::{BumpTransactionEvent, BumpTransactionEventHandler}; /// A handler for [`Event::BumpTransaction`] events that sources confirmed UTXOs from a /// [`CoinSelectionSourceSync`] to fee bump transactions via Child-Pays-For-Parent (CPFP) or diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 3dfed10d5c8..fee0624a6ea 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -77,6 +77,13 @@ pub enum FundingInfo { /// The outpoint of the funding outpoint: transaction::OutPoint, }, + /// The contributions used to for a dual funding or splice funding transaction. + Contribution { + /// UTXOs spent as inputs contributed to the funding transaction. + inputs: Vec, + /// Outputs contributed to the funding transaction. + outputs: Vec, + }, } impl_writeable_tlv_based_enum!(FundingInfo, @@ -85,6 +92,10 @@ impl_writeable_tlv_based_enum!(FundingInfo, }, (1, OutPoint) => { (1, outpoint, required) + }, + (2, Contribution) => { + (0, inputs, optional_vec), + (1, outputs, optional_vec), } ); @@ -1561,10 +1572,6 @@ pub enum Event { abandoned_funding_txo: Option, /// The features that this channel will operate with, if available. channel_type: Option, - /// UTXOs spent as inputs contributed to the splice transaction. - contributed_inputs: Vec, - /// Outputs contributed to the splice transaction. - contributed_outputs: Vec, }, /// Used to indicate to the user that they can abandon the funding transaction and recycle the /// inputs for another purpose. @@ -2326,8 +2333,6 @@ impl Writeable for Event { ref counterparty_node_id, ref abandoned_funding_txo, ref channel_type, - ref contributed_inputs, - ref contributed_outputs, } => { 52u8.write(writer)?; write_tlv_fields!(writer, { @@ -2336,8 +2341,6 @@ impl Writeable for Event { (5, user_channel_id, required), (7, counterparty_node_id, required), (9, abandoned_funding_txo, option), - (11, *contributed_inputs, optional_vec), - (13, *contributed_outputs, optional_vec), }); }, // Note that, going forward, all new events must only write data inside of @@ -2964,8 +2967,6 @@ impl MaybeReadable for Event { (5, user_channel_id, required), (7, counterparty_node_id, required), (9, abandoned_funding_txo, option), - (11, contributed_inputs, optional_vec), - (13, contributed_outputs, optional_vec), }); Ok(Some(Event::SpliceFailed { @@ -2974,8 +2975,6 @@ impl MaybeReadable for Event { counterparty_node_id: counterparty_node_id.0.unwrap(), abandoned_funding_txo, channel_type, - contributed_inputs: contributed_inputs.unwrap_or_default(), - contributed_outputs: contributed_outputs.unwrap_or_default(), })) }; f() diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs index f34a2b3275c..558812af55b 100644 --- a/lightning/src/ln/async_signer_tests.rs +++ b/lightning/src/ln/async_signer_tests.rs @@ -10,7 +10,6 @@ //! Tests for asynchronous signing. These tests verify that the channel state machine behaves //! properly with a signer implementation that asynchronously derives signatures. -use crate::events::bump_transaction::sync::WalletSourceSync; use crate::ln::splicing_tests::{initiate_splice_out, negotiate_splice_tx}; use crate::prelude::*; use crate::util::ser::Writeable; @@ -31,6 +30,7 @@ use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::SignerProvider; use crate::util::logger::Logger; use crate::util::test_channel_signer::SignerOp; +use crate::util::wallet_utils::WalletSourceSync; #[test] fn test_open_channel() { diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0f1916ac59f..67fa92c66b2 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -28,7 +28,7 @@ use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn}; use crate::blinded_path::message::BlindedMessagePath; use crate::chain::chaininterface::{ - fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, TransactionType, + ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, TransactionType, }; use crate::chain::channelmonitor::{ ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, CommitmentHTLCData, @@ -36,7 +36,6 @@ use crate::chain::channelmonitor::{ }; use crate::chain::transaction::{OutPoint, TransactionData}; use crate::chain::BestBlock; -use crate::events::bump_transaction::Input; use crate::events::{ClosureReason, FundingInfo}; use crate::ln::chan_utils; use crate::ln::chan_utils::{ @@ -58,9 +57,8 @@ use crate::ln::channelmanager::{ }; use crate::ln::funding::{FundingContribution, FundingTemplate, FundingTxInput}; use crate::ln::interactivetxs::{ - calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteValue, - InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, - InteractiveTxSigningSession, NegotiationError, SharedOwnedInput, SharedOwnedOutput, + AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, + InteractiveTxMessageSend, InteractiveTxSigningSession, SharedOwnedInput, SharedOwnedOutput, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; @@ -84,6 +82,7 @@ use crate::util::errors::APIError; use crate::util::logger::{Logger, Record, WithContext}; use crate::util::scid_utils::{block_from_scid, scid_from_parts}; use crate::util::ser::{Readable, ReadableArgs, RequiredWrapper, Writeable, Writer}; +use crate::util::wallet_utils::Input; use crate::{impl_readable_for_vec, impl_writeable_for_vec}; use alloc::collections::{btree_map, BTreeMap}; @@ -2374,6 +2373,7 @@ where holder_commitment_point, pending_splice: None, quiescent_action: None, + holder_is_quiescence_initiator: false, }; let res = funded_channel.initial_commitment_signed_v2(msg, best_block, signer_provider, logger) .map(|monitor| (Some(monitor), None)) @@ -2895,6 +2895,10 @@ struct PendingFunding { /// The funding txid used in the `splice_locked` received from the counterparty. received_funding_txid: Option, + + /// The feerate used in the last successfully negotiated funding transaction. + /// Used for validating the 25/24 feerate increase rule on RBF attempts. + last_funding_feerate_sat_per_1000_weight: Option, } impl_writeable_tlv_based!(PendingFunding, { @@ -2902,13 +2906,13 @@ impl_writeable_tlv_based!(PendingFunding, { (3, negotiated_candidates, required_vec), (5, sent_funding_txid, option), (7, received_funding_txid, option), + (9, last_funding_feerate_sat_per_1000_weight, option), }); #[derive(Debug)] enum FundingNegotiation { AwaitingAck { context: FundingNegotiationContext, - change_strategy: ChangeStrategy, new_holder_funding_key: PublicKey, }, ConstructingTransaction { @@ -2994,38 +2998,8 @@ impl PendingFunding { } } -#[derive(Debug)] -pub(crate) struct SpliceInstructions { - adjusted_funding_contribution: SignedAmount, - our_funding_inputs: Vec, - our_funding_outputs: Vec, - change_script: Option, - funding_feerate_per_kw: u32, - locktime: u32, -} - -impl SpliceInstructions { - fn into_contributed_inputs_and_outputs(self) -> (Vec, Vec) { - ( - self.our_funding_inputs.into_iter().map(|input| input.utxo.outpoint).collect(), - self.our_funding_outputs, - ) - } -} - -impl_writeable_tlv_based!(SpliceInstructions, { - (1, adjusted_funding_contribution, required), - (3, our_funding_inputs, required_vec), - (5, our_funding_outputs, required_vec), - (7, change_script, option), - (9, funding_feerate_per_kw, required), - (11, locktime, required), -}); - #[derive(Debug)] pub(crate) enum QuiescentAction { - // Deprecated in favor of the Splice variant and no longer produced as of LDK 0.3. - LegacySplice(SpliceInstructions), Splice { contribution: FundingContribution, locktime: LockTime, @@ -3034,9 +3008,35 @@ pub(crate) enum QuiescentAction { DoNothing, } +pub(super) enum QuiescentError { + DoNothing, + DiscardFunding { inputs: Vec, outputs: Vec }, + FailSplice(SpliceFundingFailed), +} + +impl From for QuiescentError { + fn from(action: QuiescentAction) -> Self { + match action { + QuiescentAction::Splice { contribution, .. } => { + let (contributed_inputs, contributed_outputs) = + contribution.into_contributed_inputs_and_outputs(); + return QuiescentError::FailSplice(SpliceFundingFailed { + funding_txo: None, + channel_type: None, + contributed_inputs, + contributed_outputs, + }); + }, + #[cfg(any(test, fuzzing))] + QuiescentAction::DoNothing => QuiescentError::DoNothing, + } + } +} + pub(crate) enum StfuResponse { Stfu(msgs::Stfu), SpliceInit(msgs::SpliceInit), + TxInitRbf(msgs::TxInitRbf), } #[cfg(any(test, fuzzing))] @@ -3046,7 +3046,6 @@ impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, (0, contribution, required), (1, locktime, required), }, - {1, LegacySplice} => (), ); #[cfg(not(any(test, fuzzing)))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, @@ -3054,7 +3053,6 @@ impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, (0, contribution, required), (1, locktime, required), }, - {1, LegacySplice} => (), ); /// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. @@ -6683,24 +6681,13 @@ pub(super) struct FundingNegotiationContext { pub our_funding_outputs: Vec, } -/// How the funding transaction's change is determined. -#[derive(Debug)] -pub(super) enum ChangeStrategy { - /// The change output, if any, is included in the FundingContribution's outputs. - FromCoinSelection, - - /// The change output script. This will be used if needed or -- if not set -- generated using - /// `SignerProvider::get_destination_script`. - LegacyUserProvided(Option), -} - impl FundingNegotiationContext { /// Prepare and start interactive transaction negotiation. /// If error occurs, it is caused by our side, not the counterparty. fn into_interactive_tx_constructor( - mut self, context: &ChannelContext, funding: &FundingScope, signer_provider: &SP, - entropy_source: &ES, holder_node_id: PublicKey, change_strategy: ChangeStrategy, - ) -> Result { + self, context: &ChannelContext, funding: &FundingScope, entropy_source: &ES, + holder_node_id: PublicKey, + ) -> InteractiveTxConstructor { debug_assert_eq!( self.shared_funding_input.is_some(), funding.channel_transaction_parameters.splice_parent_funding_txid.is_some(), @@ -6712,32 +6699,17 @@ impl FundingNegotiationContext { debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_))); } - // Note: For the error case when the inputs are insufficient, it will be handled after - // the `calculate_change_output_value` call below - let shared_funding_output = TxOut { value: Amount::from_sat(funding.get_value_satoshis()), script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; - match self.calculate_change_output( - context, - signer_provider, - &shared_funding_output, - change_strategy, - ) { - Ok(Some(change_output)) => self.our_funding_outputs.push(change_output), - Ok(None) => {}, - Err(reason) => return Err(self.into_negotiation_error(reason)), - } - let constructor_args = InteractiveTxConstructorArgs { entropy_source, holder_node_id, counterparty_node_id: context.counterparty_node_id, channel_id: context.channel_id(), feerate_sat_per_kw: self.funding_feerate_sat_per_1000_weight, - is_initiator: self.is_initiator, funding_tx_locktime: self.funding_tx_locktime, inputs_to_contribute: self.our_funding_inputs, shared_funding_input: self.shared_funding_input, @@ -6747,58 +6719,11 @@ impl FundingNegotiationContext { ), outputs_to_contribute: self.our_funding_outputs, }; - InteractiveTxConstructor::new(constructor_args) - } - - fn calculate_change_output( - &self, context: &ChannelContext, signer_provider: &SP, shared_funding_output: &TxOut, - change_strategy: ChangeStrategy, - ) -> Result, AbortReason> { - if self.our_funding_inputs.is_empty() { - return Ok(None); - } - - let change_script = match change_strategy { - ChangeStrategy::FromCoinSelection => return Ok(None), - ChangeStrategy::LegacyUserProvided(change_script) => change_script, - }; - - let change_value = calculate_change_output_value( - &self, - self.shared_funding_input.is_some(), - &shared_funding_output.script_pubkey, - context.holder_dust_limit_satoshis, - )?; - - if let Some(change_value) = change_value { - let change_script = match change_script { - Some(script) => script, - None => match signer_provider.get_destination_script(context.channel_keys_id) { - Ok(script) => script, - Err(_) => { - return Err(AbortReason::InternalError("Error getting change script")) - }, - }, - }; - let mut change_output = TxOut { value: change_value, script_pubkey: change_script }; - let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); - let change_output_fee = - fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight); - let change_value_decreased_with_fee = - change_value.to_sat().saturating_sub(change_output_fee); - // Check dust limit again - if change_value_decreased_with_fee > context.holder_dust_limit_satoshis { - change_output.value = Amount::from_sat(change_value_decreased_with_fee); - return Ok(Some(change_output)); - } + if self.is_initiator { + InteractiveTxConstructor::new_for_outbound(constructor_args) + } else { + InteractiveTxConstructor::new_for_inbound(constructor_args) } - - Ok(None) - } - - fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { - let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); - NegotiationError { reason, contributed_inputs, contributed_outputs } } fn into_contributed_inputs_and_outputs(self) -> (Vec, Vec) { @@ -6832,6 +6757,10 @@ pub(super) struct FundedChannel { /// initiator we may be able to merge this action into what the counterparty wanted to do (e.g. /// in the case of splicing). quiescent_action: Option, + + /// Whether we (the holder) initiated the current quiescence session. + /// Set when quiescence is established, cleared when quiescence ends. + holder_is_quiescence_initiator: bool, } #[cfg(any(test, fuzzing))] @@ -7046,16 +6975,6 @@ where self.reset_pending_splice_state() } else { match self.quiescent_action.take() { - Some(QuiescentAction::LegacySplice(instructions)) => { - self.context.channel_state.clear_awaiting_quiescence(); - let (inputs, outputs) = instructions.into_contributed_inputs_and_outputs(); - Some(SpliceFundingFailed { - funding_txo: None, - channel_type: None, - contributed_inputs: inputs, - contributed_outputs: outputs, - }) - }, Some(QuiescentAction::Splice { contribution, .. }) => { self.context.channel_state.clear_awaiting_quiescence(); let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs(); @@ -11488,10 +11407,7 @@ where if let Some(quiescent_action) = self.quiescent_action.as_ref() { // TODO(splicing): If we didn't win quiescence, then we can contribute as an acceptor // instead of waiting for the splice to lock. - if matches!( - quiescent_action, - QuiescentAction::Splice { .. } | QuiescentAction::LegacySplice(_) - ) { + if matches!(quiescent_action, QuiescentAction::Splice { .. }) { self.context.channel_state.set_awaiting_quiescence(); } } @@ -12136,7 +12052,7 @@ where } /// Initiate splicing. - pub fn splice_channel(&mut self, feerate: FeeRate) -> Result { + pub fn splice_channel(&self, feerate: FeeRate) -> Result { if self.holder_commitment_point.current_point().is_none() { return Err(APIError::APIMisuseError { err: format!( @@ -12187,17 +12103,143 @@ where satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT, }; - Ok(FundingTemplate::new(Some(shared_input), feerate, true)) + Ok(FundingTemplate::new(Some(shared_input), feerate)) + } + + /// Initiate an RBF of a pending splice transaction. + pub fn rbf_channel(&self, feerate: FeeRate) -> Result { + if self.holder_commitment_point.current_point().is_none() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot RBF until a payment is routed", + self.context.channel_id(), + ), + }); + } + + if self.quiescent_action.is_some() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot RBF as one is waiting to be negotiated", + self.context.channel_id(), + ), + }); + } + + if !self.context.is_usable() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot RBF as it is either pending open/close", + self.context.channel_id() + ), + }); + } + + if self.context.minimum_depth(&self.funding) == Some(0) { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} has option_zeroconf, cannot RBF splice", + self.context.channel_id(), + ), + }); + } + + let pending_splice = match &self.pending_splice { + Some(pending_splice) => pending_splice, + None => { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} has no pending splice to RBF", + self.context.channel_id(), + ), + }); + }, + }; + + if pending_splice.funding_negotiation.is_some() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot RBF as a funding negotiation is already in progress", + self.context.channel_id(), + ), + }); + } + + if pending_splice.sent_funding_txid.is_some() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} already sent splice_locked, cannot RBF", + self.context.channel_id(), + ), + }); + } + + if pending_splice.negotiated_candidates.is_empty() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} has no negotiated splice candidates to RBF", + self.context.channel_id(), + ), + }); + } + + // Check the 25/24 feerate increase rule + let new_feerate = feerate.to_sat_per_kwu() as u32; + if let Some(prev_feerate) = pending_splice.last_funding_feerate_sat_per_1000_weight { + if (new_feerate as u64) * 24 < (prev_feerate as u64) * 25 { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} RBF feerate {} is less than 25/24 of the previous feerate {}", + self.context.channel_id(), + new_feerate, + prev_feerate, + ), + }); + } + } + + 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, + }; + + Ok(FundingTemplate::new(Some(shared_input), feerate)) } pub fn funding_contributed( &mut self, contribution: FundingContribution, locktime: LockTime, logger: &L, - ) -> Result, SpliceFundingFailed> { + ) -> Result, QuiescentError> { debug_assert!(contribution.is_splice()); - if let Err(e) = contribution.net_value().and_then(|our_funding_contribution| { + if let Some(QuiescentAction::Splice { contribution: existing, .. }) = &self.quiescent_action + { + let (new_inputs, new_outputs) = contribution.into_contributed_inputs_and_outputs(); + + // Filter out inputs/outputs already in the existing contribution + let inputs: Vec<_> = new_inputs + .into_iter() + .filter(|input| !existing.contributed_inputs().any(|e| e == *input)) + .collect(); + let outputs: Vec<_> = new_outputs + .into_iter() + .filter(|output| !existing.contributed_outputs().any(|e| *e == *output)) + .collect(); + + if inputs.is_empty() && outputs.is_empty() { + return Err(QuiescentError::DoNothing); + } + + return Err(QuiescentError::DiscardFunding { inputs, outputs }); + } + + if let Err(e) = contribution.validate().and_then(|()| { // For splice-out, our_funding_contribution is adjusted to cover fees if there // aren't any inputs. + let our_funding_contribution = contribution.net_value(); self.validate_splice_contributions(our_funding_contribution, SignedAmount::ZERO) }) { log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e); @@ -12205,65 +12247,19 @@ where let (contributed_inputs, contributed_outputs) = contribution.into_contributed_inputs_and_outputs(); - return Err(SpliceFundingFailed { + return Err(QuiescentError::FailSplice(SpliceFundingFailed { funding_txo: None, channel_type: None, contributed_inputs, contributed_outputs, - }); + })); } - self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime }).map_err( - |action| { - // FIXME: Any better way to do this? - if let QuiescentAction::Splice { contribution, .. } = action { - let (contributed_inputs, contributed_outputs) = - contribution.into_contributed_inputs_and_outputs(); - SpliceFundingFailed { - funding_txo: None, - channel_type: None, - contributed_inputs, - contributed_outputs, - } - } else { - debug_assert!(false); - SpliceFundingFailed { - funding_txo: None, - channel_type: None, - contributed_inputs: vec![], - contributed_outputs: vec![], - } - } - }, - ) - } - - fn send_splice_init(&mut self, instructions: SpliceInstructions) -> msgs::SpliceInit { - let SpliceInstructions { - adjusted_funding_contribution, - our_funding_inputs, - our_funding_outputs, - change_script, - funding_feerate_per_kw, - locktime, - } = instructions; - - let prev_funding_input = self.funding.to_splice_funding_input(); - let context = FundingNegotiationContext { - is_initiator: true, - 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, - }; - - self.send_splice_init_internal(context, ChangeStrategy::LegacyUserProvided(change_script)) + self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime }) } fn send_splice_init_internal( - &mut self, context: FundingNegotiationContext, change_strategy: ChangeStrategy, + &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 @@ -12284,16 +12280,14 @@ where let funding_contribution_satoshis = context.our_funding_contribution.to_sat(); let locktime = context.funding_tx_locktime.to_consensus_u32(); - let funding_negotiation = FundingNegotiation::AwaitingAck { - context, - change_strategy, - new_holder_funding_key: funding_pubkey, - }; + let funding_negotiation = + FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey }; self.pending_splice = Some(PendingFunding { funding_negotiation: Some(funding_negotiation), negotiated_candidates: vec![], sent_funding_txid: None, received_funding_txid: None, + last_funding_feerate_sat_per_1000_weight: Some(funding_feerate_per_kw), }); msgs::SpliceInit { @@ -12306,6 +12300,34 @@ where } } + fn send_tx_init_rbf_internal(&mut self, context: FundingNegotiationContext) -> msgs::TxInitRbf { + let pending_splice = + self.pending_splice.as_mut().expect("pending_splice should exist for RBF"); + debug_assert!(!pending_splice.negotiated_candidates.is_empty()); + + let new_holder_funding_key = pending_splice + .negotiated_candidates + .first() + .unwrap() + .get_holder_pubkeys() + .funding_pubkey; + + let funding_feerate_per_kw = context.funding_feerate_sat_per_1000_weight; + let funding_contribution_satoshis = context.our_funding_contribution.to_sat(); + let locktime = context.funding_tx_locktime.to_consensus_u32(); + + pending_splice.funding_negotiation = + Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key }); + pending_splice.last_funding_feerate_sat_per_1000_weight = Some(funding_feerate_per_kw); + + msgs::TxInitRbf { + channel_id: self.context.channel_id, + locktime, + feerate_sat_per_1000_weight: funding_feerate_per_kw, + funding_output_contribution: Some(funding_contribution_satoshis), + } + } + #[cfg(test)] pub fn abandon_splice( &mut self, @@ -12364,10 +12386,6 @@ where )); } - // TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient, - // similarly to the check in `funding_contributed`. - debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO); - let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); if their_funding_contribution == SignedAmount::ZERO { return Err(ChannelError::WarnAndDisconnect(format!( @@ -12490,13 +12508,74 @@ where Ok(()) } + /// Peeks at the pending [`QuiescentAction`] without consuming it, returning the contribution + /// amount at the target feerate and whether the feerate is compatible. If feerate adjustment + /// fails, the action is preserved for a future splice and we return a zero contribution. + fn peek_pending_contribution( + &self, target_feerate: FeeRate, logger: &L, + ) -> Result<(SignedAmount, bool), ChannelError> { + match &self.quiescent_action { + Some(QuiescentAction::Splice { contribution, .. }) => { + contribution.validate().map_err(|e| { + debug_assert!(false); + ChannelError::WarnAndDisconnect(format!( + "Internal Error: Insufficient funding contribution: {}", + e, + )) + })?; + match contribution.net_value_at_feerate(target_feerate) { + Ok(net) => Ok((net, true)), + Err(e) => { + log_info!( + logger, + "Cannot accommodate initiator's feerate for channel {}: {}; \ + proceeding without contribution", + self.context.channel_id(), + e, + ); + Ok((SignedAmount::ZERO, false)) + }, + } + }, + #[cfg(any(test, fuzzing))] + Some(QuiescentAction::DoNothing) => Ok((SignedAmount::ZERO, false)), + None => Ok((SignedAmount::ZERO, false)), + } + } + + /// Consumes the pending [`QuiescentAction`], adjusting the contribution for the target + /// feerate and returning the funding inputs and outputs. Must only be called after + /// [`peek_pending_contribution`] returned `feerate_compatible = true`. + fn take_pending_contribution( + &mut self, target_feerate: FeeRate, + ) -> (Vec, Vec) { + match self.quiescent_action.take() { + Some(QuiescentAction::Splice { mut contribution, .. }) => { + contribution + .adjust_for_feerate(target_feerate) + .expect("feerate compatibility already checked in peek_pending_contribution"); + contribution.into_tx_parts() + }, + _ => unreachable!("take_pending_contribution called without compatible contribution"), + } + } + pub(crate) fn splice_init( - &mut self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64, - signer_provider: &SP, entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, + &mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey, + logger: &L, ) -> Result { - let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis); + let target_feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64); + let (our_funding_contribution, feerate_compatible) = + self.peek_pending_contribution(target_feerate, logger)?; + let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?; + let (our_funding_inputs, our_funding_outputs) = if feerate_compatible { + self.take_pending_contribution(target_feerate) + } else { + (Vec::new(), Vec::new()) + }; + log_info!( logger, "Starting splice funding negotiation for channel {} after receiving splice_init; new channel value: {} sats (old: {} sats)", @@ -12512,33 +12591,19 @@ where funding_tx_locktime: LockTime::from_consensus(msg.locktime), funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw, shared_funding_input: Some(prev_funding_input), - our_funding_inputs: Vec::new(), - our_funding_outputs: Vec::new(), + our_funding_inputs, + our_funding_outputs, }; let mut interactive_tx_constructor = funding_negotiation_context .into_interactive_tx_constructor( &self.context, &splice_funding, - signer_provider, entropy_source, holder_node_id.clone(), - // ChangeStrategy doesn't matter when no inputs are contributed - ChangeStrategy::FromCoinSelection, - ) - .map_err(|err| { - ChannelError::WarnAndDisconnect(format!( - "Failed to start interactive transaction construction, {:?}", - err - )) - })?; + ); debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none()); - // TODO(splicing): if quiescent_action is set, integrate what the user wants to do into the - // counterparty-initiated splice. For always-on nodes this probably isn't a useful - // optimization, but for often-offline nodes it may be, as we may connect and immediately - // go into splicing from both sides. - let new_funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; self.pending_splice = Some(PendingFunding { funding_negotiation: Some(FundingNegotiation::ConstructingTransaction { @@ -12548,6 +12613,7 @@ where negotiated_candidates: Vec::new(), received_funding_txid: None, sent_funding_txid: None, + last_funding_feerate_sat_per_1000_weight: Some(msg.funding_feerate_per_kw), }); Ok(msgs::SpliceAck { @@ -12558,9 +12624,261 @@ where }) } + /// Checks during handling tx_init_rbf for an existing splice + fn validate_tx_init_rbf( + &self, msg: &msgs::TxInitRbf, our_funding_contribution: SignedAmount, + fee_estimator: &LowerBoundedFeeEstimator, + ) -> Result { + if self.holder_commitment_point.current_point().is_none() { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} commitment point needs to be advanced once before RBF", + self.context.channel_id(), + ))); + } + + if !self.context.channel_state.is_quiescent() { + return Err(ChannelError::WarnAndDisconnect("Quiescence needed for RBF".to_owned())); + } + + if self.holder_is_quiescence_initiator { + return Err(ChannelError::WarnAndDisconnect( + "Counterparty sent tx_init_rbf but is not the quiescence initiator".to_owned(), + )); + } + + if self.context.minimum_depth(&self.funding) == Some(0) { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} has option_zeroconf, cannot RBF splice", + self.context.channel_id(), + ))); + } + + let pending_splice = match &self.pending_splice { + Some(pending_splice) => pending_splice, + None => { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} has no pending splice to RBF", + self.context.channel_id(), + ))); + }, + }; + + if pending_splice.funding_negotiation.is_some() { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} already has a funding negotiation in progress", + self.context.channel_id(), + ))); + } + + if pending_splice.received_funding_txid.is_some() { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} counterparty already sent splice_locked, cannot RBF", + self.context.channel_id(), + ))); + } + + let first_candidate = match pending_splice.negotiated_candidates.first() { + Some(candidate) => candidate, + None => { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} has no negotiated splice candidates to RBF", + self.context.channel_id(), + ))); + }, + }; + + // Check the 25/24 feerate increase rule + let prev_feerate = + pending_splice.last_funding_feerate_sat_per_1000_weight.unwrap_or_else(|| { + fee_estimator.bounded_sat_per_1000_weight(ConfirmationTarget::UrgentOnChainSweep) + }); + let new_feerate = msg.feerate_sat_per_1000_weight; + if (new_feerate as u64) * 24 < (prev_feerate as u64) * 25 { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} RBF feerate {} is less than 25/24 of the previous feerate {}", + self.context.channel_id(), + new_feerate, + prev_feerate, + ))); + } + + let their_funding_contribution = match msg.funding_output_contribution { + Some(value) => SignedAmount::from_sat(value), + None => SignedAmount::ZERO, + }; + + self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; + + // Reuse funding pubkeys from the first negotiated candidate since all RBF candidates + // for the same splice share the same funding output script. + let holder_pubkeys = first_candidate.get_holder_pubkeys().clone(); + let counterparty_funding_pubkey = *first_candidate.counterparty_funding_pubkey(); + + Ok(FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + holder_pubkeys, + )) + } + + pub(crate) fn tx_init_rbf( + &mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey, + fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + ) -> Result { + let target_feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64); + let (our_funding_contribution, feerate_compatible) = + self.peek_pending_contribution(target_feerate, logger)?; + + let rbf_funding = + self.validate_tx_init_rbf(msg, our_funding_contribution, fee_estimator)?; + + let (our_funding_inputs, our_funding_outputs) = if feerate_compatible { + self.take_pending_contribution(target_feerate) + } else { + (Vec::new(), Vec::new()) + }; + + log_info!( + logger, + "Starting RBF funding negotiation for channel {} after receiving tx_init_rbf; channel value: {} sats", + self.context.channel_id, + rbf_funding.get_value_satoshis(), + ); + + let prev_funding_input = self.funding.to_splice_funding_input(); + let funding_negotiation_context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution, + funding_tx_locktime: LockTime::from_consensus(msg.locktime), + funding_feerate_sat_per_1000_weight: msg.feerate_sat_per_1000_weight, + shared_funding_input: Some(prev_funding_input), + our_funding_inputs, + our_funding_outputs, + }; + + let mut interactive_tx_constructor = funding_negotiation_context + .into_interactive_tx_constructor( + &self.context, + &rbf_funding, + entropy_source, + holder_node_id.clone(), + ); + debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none()); + + let pending_splice = self + .pending_splice + .as_mut() + .expect("We validated pending_splice exists in validate_tx_init_rbf"); + pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction { + funding: rbf_funding, + interactive_tx_constructor, + }); + pending_splice.last_funding_feerate_sat_per_1000_weight = + Some(msg.feerate_sat_per_1000_weight); + + Ok(msgs::TxAckRbf { + channel_id: self.context.channel_id, + funding_output_contribution: if our_funding_contribution != SignedAmount::ZERO { + Some(our_funding_contribution.to_sat()) + } else { + None + }, + }) + } + + fn validate_tx_ack_rbf(&self, msg: &msgs::TxAckRbf) -> Result { + let pending_splice = self + .pending_splice + .as_ref() + .ok_or_else(|| ChannelError::Ignore("Channel is not in pending splice".to_owned()))?; + + let funding_negotiation_context = match &pending_splice.funding_negotiation { + Some(FundingNegotiation::AwaitingAck { context, .. }) => context, + Some(FundingNegotiation::ConstructingTransaction { .. }) + | Some(FundingNegotiation::AwaitingSignatures { .. }) => { + return Err(ChannelError::WarnAndDisconnect( + "Got unexpected tx_ack_rbf; funding negotiation already in progress".to_owned(), + )); + }, + None => { + return Err(ChannelError::Ignore( + "Got unexpected tx_ack_rbf; no funding negotiation in progress".to_owned(), + )); + }, + }; + + let our_funding_contribution = funding_negotiation_context.our_funding_contribution; + let their_funding_contribution = match msg.funding_output_contribution { + Some(value) => SignedAmount::from_sat(value), + None => SignedAmount::ZERO, + }; + self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; + + let first_candidate = pending_splice.negotiated_candidates.first().ok_or_else(|| { + ChannelError::WarnAndDisconnect("No negotiated splice candidates for RBF".to_owned()) + })?; + let holder_pubkeys = first_candidate.get_holder_pubkeys().clone(); + let counterparty_funding_pubkey = *first_candidate.counterparty_funding_pubkey(); + + Ok(FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + holder_pubkeys, + )) + } + + pub(crate) fn tx_ack_rbf( + &mut self, msg: &msgs::TxAckRbf, entropy_source: &ES, holder_node_id: &PublicKey, + logger: &L, + ) -> Result, ChannelError> { + let rbf_funding = self.validate_tx_ack_rbf(msg)?; + + log_info!( + logger, + "Starting RBF funding negotiation for channel {} after receiving tx_ack_rbf; channel value: {} sats", + self.context.channel_id, + rbf_funding.get_value_satoshis(), + ); + + let pending_splice = + self.pending_splice.as_mut().expect("We should have returned an error earlier!"); + let funding_negotiation_context = + if let Some(FundingNegotiation::AwaitingAck { context, .. }) = + pending_splice.funding_negotiation.take() + { + context + } else { + panic!("We should have returned an error earlier!"); + }; + + let mut interactive_tx_constructor = funding_negotiation_context + .into_interactive_tx_constructor( + &self.context, + &rbf_funding, + entropy_source, + holder_node_id.clone(), + ); + let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); + + pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction { + funding: rbf_funding, + interactive_tx_constructor, + }); + + Ok(tx_msg_opt) + } + pub(crate) fn splice_ack( - &mut self, msg: &msgs::SpliceAck, signer_provider: &SP, entropy_source: &ES, - holder_node_id: &PublicKey, logger: &L, + &mut self, msg: &msgs::SpliceAck, entropy_source: &ES, holder_node_id: &PublicKey, + logger: &L, ) -> Result, ChannelError> { let splice_funding = self.validate_splice_ack(msg)?; @@ -12575,11 +12893,11 @@ where let pending_splice = self.pending_splice.as_mut().expect("We should have returned an error earlier!"); // TODO: Good candidate for a let else statement once MSRV >= 1.65 - let (funding_negotiation_context, change_strategy) = - if let Some(FundingNegotiation::AwaitingAck { context, change_strategy, .. }) = + let funding_negotiation_context = + if let Some(FundingNegotiation::AwaitingAck { context, .. }) = pending_splice.funding_negotiation.take() { - (context, change_strategy) + context } else { panic!("We should have returned an error earlier!"); }; @@ -12588,17 +12906,9 @@ where .into_interactive_tx_constructor( &self.context, &splice_funding, - signer_provider, entropy_source, holder_node_id.clone(), - change_strategy, - ) - .map_err(|err| { - ChannelError::WarnAndDisconnect(format!( - "Failed to start interactive transaction construction, {:?}", - err - )) - })?; + ); let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); debug_assert!(self.context.interactive_tx_signing_session.is_none()); @@ -13354,19 +13664,19 @@ where #[rustfmt::skip] pub fn propose_quiescence( &mut self, logger: &L, action: QuiescentAction, - ) -> Result, QuiescentAction> { + ) -> Result, QuiescentError> { log_debug!(logger, "Attempting to initiate quiescence"); if !self.context.is_usable() { log_debug!(logger, "Channel is not in a usable state to propose quiescence"); - return Err(action); + return Err(action.into()); } if self.quiescent_action.is_some() { log_debug!( logger, "Channel already has a pending quiescent action and cannot start another", ); - return Err(action); + return Err(action.into()); } self.quiescent_action = Some(action); @@ -13414,6 +13724,7 @@ where self.context.channel_state.clear_awaiting_quiescence(); self.context.channel_state.clear_remote_stfu_sent(); self.context.channel_state.set_quiescent(); + self.holder_is_quiescence_initiator = false; // We are sending an stfu in response to our couterparty's stfu, but had not yet sent // our own stfu (even if `awaiting_quiescence` was set). Thus, the counterparty is the // initiator and they can do "something fundamental". @@ -13492,6 +13803,7 @@ where self.context.channel_state.clear_local_stfu_sent(); self.context.channel_state.set_quiescent(); + self.holder_is_quiescence_initiator = is_holder_quiescence_initiator; log_debug!( logger, @@ -13507,57 +13819,14 @@ where "Internal Error: Didn't have anything to do after reaching quiescence".to_owned() )); }, - Some(QuiescentAction::LegacySplice(instructions)) => { - if self.pending_splice.is_some() { - debug_assert!(false); - self.quiescent_action = Some(QuiescentAction::LegacySplice(instructions)); - - return Err(ChannelError::WarnAndDisconnect( - format!( - "Channel {} cannot be spliced as it already has a splice pending", - self.context.channel_id(), - ), - )); - } - - let splice_init = self.send_splice_init(instructions); - return Ok(Some(StfuResponse::SpliceInit(splice_init))); - }, Some(QuiescentAction::Splice { contribution, locktime }) => { - // TODO(splicing): If the splice has been negotiated but has not been locked, we - // can RBF here to add the contribution. - if self.pending_splice.is_some() { - debug_assert!(false); - self.quiescent_action = - Some(QuiescentAction::Splice { contribution, locktime }); - - return Err(ChannelError::WarnAndDisconnect( - format!( - "Channel {} cannot be spliced as it already has a splice pending", - self.context.channel_id(), - ), - )); - } - let prev_funding_input = self.funding.to_splice_funding_input(); - let is_initiator = contribution.is_initiator(); - let our_funding_contribution = match contribution.net_value() { - Ok(net_value) => net_value, - Err(e) => { - debug_assert!(false); - return Err(ChannelError::WarnAndDisconnect( - format!( - "Internal Error: Insufficient funding contribution: {}", - e, - ) - )); - }, - }; + let our_funding_contribution = contribution.net_value(); let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32; let (our_funding_inputs, our_funding_outputs) = contribution.into_tx_parts(); let context = FundingNegotiationContext { - is_initiator, + is_initiator: true, our_funding_contribution, funding_tx_locktime: locktime, funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, @@ -13566,7 +13835,12 @@ where our_funding_outputs, }; - let splice_init = self.send_splice_init_internal(context, ChangeStrategy::FromCoinSelection); + if self.pending_splice.is_some() { + let tx_init_rbf = self.send_tx_init_rbf_internal(context); + return Ok(Some(StfuResponse::TxInitRbf(tx_init_rbf))); + } + + let splice_init = self.send_splice_init_internal(context); return Ok(Some(StfuResponse::SpliceInit(splice_init))); }, #[cfg(any(test, fuzzing))] @@ -13946,6 +14220,7 @@ impl OutboundV1Channel { holder_commitment_point, pending_splice: None, quiescent_action: None, + holder_is_quiescence_initiator: false, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() @@ -14246,6 +14521,7 @@ impl InboundV1Channel { holder_commitment_point, pending_splice: None, quiescent_action: None, + holder_is_quiescence_initiator: false, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() || channel.context.signer_pending_channel_ready; @@ -14502,7 +14778,7 @@ impl PendingV2Channel { script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; - let interactive_tx_constructor = Some(InteractiveTxConstructor::new( + let interactive_tx_constructor = Some(InteractiveTxConstructor::new_for_inbound( InteractiveTxConstructorArgs { entropy_source, holder_node_id, @@ -14510,16 +14786,12 @@ impl PendingV2Channel { channel_id: context.channel_id, feerate_sat_per_kw: funding_negotiation_context.funding_feerate_sat_per_1000_weight, funding_tx_locktime: funding_negotiation_context.funding_tx_locktime, - is_initiator: false, inputs_to_contribute: our_funding_inputs, shared_funding_input: None, shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats), outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(), } - ).map_err(|err| { - let reason = ClosureReason::ProcessingError { err: err.reason.to_string() }; - ChannelError::Close((err.reason.to_string(), reason)) - })?); + )); let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, @@ -15173,7 +15445,7 @@ impl Writeable for FundedChannel { (61, fulfill_attribution_data, optional_vec), // Added in 0.2 (63, holder_commitment_point_current, option), // Added in 0.2 (64, pending_splice, option), // Added in 0.2 - (65, self.quiescent_action, option), // Added in 0.2 + // 65 was previously used for quiescent_action (67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2 (71, holder_commitment_point_previous_revoked, option), // Added in 0.3 @@ -15563,7 +15835,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> let mut minimum_depth_override: Option = None; let mut pending_splice: Option = None; - let mut quiescent_action = None; + let mut _quiescent_action: Option = None; let mut pending_outbound_held_htlc_flags_opt: Option>> = None; let mut holding_cell_held_htlc_flags_opt: Option>> = None; @@ -15617,7 +15889,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> (61, fulfill_attribution_data, optional_vec), // Added in 0.2 (63, holder_commitment_point_current_opt, option), // Added in 0.2 (64, pending_splice, option), // Added in 0.2 - (65, quiescent_action, upgradable_option), // Added in 0.2 + (65, _quiescent_action, upgradable_option), // Added in 0.2 (67, pending_outbound_held_htlc_flags_opt, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2 (71, holder_commitment_point_previous_revoked_opt, option), // Added in 0.3 @@ -15943,6 +16215,12 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> } } + // quiescent_action is no longer persisted, so clear the awaiting_quiescence flag if set. + let mut channel_state = channel_state; + if channel_state.is_awaiting_quiescence() { + channel_state.clear_awaiting_quiescence(); + } + Ok(FundedChannel { funding: FundingScope { value_to_self_msat, @@ -16082,7 +16360,8 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> }, holder_commitment_point, pending_splice, - quiescent_action, + quiescent_action: None, + holder_is_quiescence_initiator: false, }) } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 75de6ab5d10..b68facb840c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -57,6 +57,7 @@ use crate::events::{FundingInfo, PaidBolt12Invoice}; use crate::ln::chan_utils::selected_commitment_sat_per_1000_weight; #[cfg(any(test, fuzzing))] use crate::ln::channel::QuiescentAction; +use crate::ln::channel::QuiescentError; use crate::ln::channel::{ self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, DisconnectResult, FundedChannel, FundingTxSigned, InboundV1Channel, OutboundHop, OutboundV1Channel, @@ -4170,8 +4171,16 @@ impl< user_channel_id: shutdown_res.user_channel_id, abandoned_funding_txo: splice_funding_failed.funding_txo, channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + }, + None, + )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: shutdown_res.channel_id, + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }, None, )); @@ -4577,18 +4586,56 @@ impl< pub fn splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, feerate: FeeRate, ) -> Result { - let mut res = Err(APIError::APIMisuseError { err: String::new() }); - PersistenceNotifierGuard::optionally_notify(self, || { - let result = self.internal_splice_channel( - channel_id, counterparty_node_id, feerate, - ); - res = result; - NotifyOption::SkipPersistNoEvents - }); - res + let per_peer_state = self.per_peer_state.read().unwrap(); + + let peer_state_mutex = match per_peer_state + .get(counterparty_node_id) + .ok_or_else(|| APIError::no_such_peer(counterparty_node_id)) + { + Ok(p) => p, + Err(e) => return Err(e), + }; + + let mut peer_state = peer_state_mutex.lock().unwrap(); + if !peer_state.latest_features.supports_splicing() { + return Err(APIError::ChannelUnavailable { + err: "Peer does not support splicing".to_owned(), + }); + } + if !peer_state.latest_features.supports_quiescence() { + return Err(APIError::ChannelUnavailable { + err: "Peer does not support quiescence, a splicing prerequisite".to_owned(), + }); + } + + // Look for the channel + match peer_state.channel_by_id.entry(*channel_id) { + hash_map::Entry::Occupied(chan_phase_entry) => { + if let Some(chan) = chan_phase_entry.get().as_funded() { + chan.splice_channel(feerate) + } else { + Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} is not funded, cannot splice it", + channel_id + ), + }) + } + }, + hash_map::Entry::Vacant(_) => { + Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)) + }, + } } - fn internal_splice_channel( + /// Initiate an RBF of a pending splice transaction for an existing channel. + /// + /// This is used after a splice has been negotiated but before it has been locked, in order + /// to bump the feerate of the funding transaction via replace-by-fee. + /// + /// Returns a [`FundingTemplate`] that must be completed with inputs/outputs and then + /// passed to [`Self::funding_contributed`]. + pub fn rbf_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, feerate: FeeRate, ) -> Result { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -4615,13 +4662,13 @@ impl< // Look for the channel match peer_state.channel_by_id.entry(*channel_id) { - hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { - chan.splice_channel(feerate) + hash_map::Entry::Occupied(chan_phase_entry) => { + if let Some(chan) = chan_phase_entry.get().as_funded() { + chan.rbf_channel(feerate) } else { Err(APIError::ChannelUnavailable { err: format!( - "Channel with id {} is not funded, cannot splice it", + "Channel with id {} is not funded, cannot RBF splice", channel_id ), }) @@ -4694,8 +4741,16 @@ impl< user_channel_id: chan.context.get_user_id(), abandoned_funding_txo: splice_funding_failed.funding_txo, channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + }, + None, + )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }, None, )); @@ -6356,6 +6411,15 @@ impl< 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() { + let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs(); + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }, + None, + )); result = Err(APIError::ChannelUnavailable { err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") }); @@ -6382,28 +6446,78 @@ impl< ); } }, - Err(splice_funding_failed) => { + Err(QuiescentError::DoNothing) => { + result = Err(APIError::APIMisuseError { + err: format!( + "Duplicate funding contribution for channel {}", + channel_id + ), + }); + }, + Err(QuiescentError::DiscardFunding { inputs, outputs }) => { + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }, + None, + )); + result = Err(APIError::APIMisuseError { + err: format!( + "Channel {} already has a pending funding contribution", + channel_id + ), + }); + }, + Err(QuiescentError::FailSplice(SpliceFundingFailed { + funding_txo, + channel_type, + contributed_inputs, + contributed_outputs, + })) => { 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, + abandoned_funding_txo: funding_txo, + channel_type, }, None, )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { + inputs: contributed_inputs, + outputs: contributed_outputs, + }, + }, + None, + )); + result = Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot accept funding contribution", + channel_id + ), + }); }, } return NotifyOption::DoPersist; }, None => { + let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs(); + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }, + None, + )); result = Err(APIError::APIMisuseError { err: format!( "Channel with id {} not expecting funding contribution", @@ -6414,6 +6528,15 @@ impl< }, }, None => { + let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs(); + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }, + None, + )); result = Err(APIError::ChannelUnavailable { err: format!( "Channel with id {} not found for the passed counterparty node_id {}", @@ -11334,8 +11457,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ 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, + )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id, + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }, None, )); @@ -11480,8 +11611,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ user_channel_id: chan.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, + )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: msg.channel_id, + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }, None, )); @@ -11624,8 +11763,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ user_channel_id: chan_entry.get().context().get_user_id(), abandoned_funding_txo: splice_funding_failed.funding_txo, channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + }, + None, + )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: msg.channel_id, + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }, None, )); @@ -12422,6 +12569,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); Ok(true) }, + Some(StfuResponse::TxInitRbf(msg)) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendTxInitRbf { + node_id: *counterparty_node_id, + msg, + }); + Ok(true) + }, } } else { let msg = "Peer sent `stfu` for an unfunded channel"; @@ -12661,9 +12815,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side - let our_funding_contribution = 0i64; - // Look for the channel match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Vacant(_) => { @@ -12683,8 +12834,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { let init_res = funded_channel.splice_init( msg, - our_funding_contribution, - &self.signer_provider, &self.entropy_source, &self.get_our_node_id(), &self.logger, @@ -12707,6 +12856,54 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + /// Handle incoming tx_init_rbf, start a new round of interactive transaction construction. + fn internal_tx_init_rbf( + &self, counterparty_node_id: &PublicKey, msg: &msgs::TxInitRbf, + ) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::no_such_peer(counterparty_node_id, msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => { + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )) + }, + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { + let init_res = funded_channel.tx_init_rbf( + msg, + &self.entropy_source, + &self.get_our_node_id(), + &self.fee_estimator, + &self.logger, + ); + let tx_ack_rbf_msg = try_channel_entry!(self, peer_state, init_res, chan_entry); + peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf { + node_id: *counterparty_node_id, + msg: tx_ack_rbf_msg, + }); + Ok(()) + } else { + try_channel_entry!( + self, + peer_state, + Err( + ChannelError::close("Channel is not funded, cannot RBF splice".into(),) + ), + chan_entry + ) + } + }, + } + } + /// Handle incoming splice request ack, transition channel to splice-pending (unless some check fails). fn internal_splice_ack( &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceAck, @@ -12729,7 +12926,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { let splice_ack_res = funded_channel.splice_ack( msg, - &self.signer_provider, &self.entropy_source, &self.get_our_node_id(), &self.logger, @@ -12754,6 +12950,51 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + fn internal_tx_ack_rbf( + &self, counterparty_node_id: &PublicKey, msg: &msgs::TxAckRbf, + ) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::no_such_peer(counterparty_node_id, msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )), + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { + let tx_ack_rbf_res = funded_channel.tx_ack_rbf( + msg, + &self.entropy_source, + &self.get_our_node_id(), + &self.logger, + ); + let tx_msg_opt = + try_channel_entry!(self, peer_state, tx_ack_rbf_res, chan_entry); + if let Some(tx_msg) = tx_msg_opt { + peer_state + .pending_msg_events + .push(tx_msg.into_msg_send_event(counterparty_node_id.clone())); + } + Ok(()) + } else { + try_channel_entry!( + self, + peer_state, + Err(ChannelError::close("Channel is not funded, cannot RBF splice".into())), + chan_entry + ) + } + }, + } + } + fn internal_splice_locked( &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceLocked, ) -> Result<(), MsgHandleErrInternal> { @@ -13401,7 +13642,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); notify = NotifyOption::SkipPersistHandleEvents; }, - Err(action) => log_trace!(logger, "Failed to propose quiescence for: {:?}", action), + Err(e) => { + debug_assert!(matches!(e, QuiescentError::DoNothing)); + log_trace!(logger, "Failed to propose quiescence"); + }, } } else { result = Err(APIError::APIMisuseError { @@ -14756,8 +15000,13 @@ impl< user_channel_id: chan.context().get_user_id(), abandoned_funding_txo: splice_funding_failed.funding_txo, channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + }); + splice_failed_events.push(events::Event::DiscardFunding { + channel_id: chan.context().channel_id(), + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }); } @@ -16176,19 +16425,29 @@ impl< } fn handle_tx_init_rbf(&self, counterparty_node_id: PublicKey, msg: &msgs::TxInitRbf) { - let err = Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone(), - )); - let _: Result<(), _> = self.handle_error(err, counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_tx_init_rbf(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistHandleEvents, + }; + let _ = self.handle_error(res, counterparty_node_id); + persist + }); } fn handle_tx_ack_rbf(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAckRbf) { - let err = Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone(), - )); - let _: Result<(), _> = self.handle_error(err, counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_tx_ack_rbf(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistHandleEvents, + }; + let _ = self.handle_error(res, counterparty_node_id); + persist + }); } fn handle_tx_abort(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAbort) { @@ -17371,9 +17630,9 @@ impl< let our_pending_intercepts = self.pending_intercepted_htlcs.lock().unwrap(); // Since some FundingNegotiation variants are not persisted, any splice in such state must - // be failed upon reload. However, as the necessary information for the SpliceFailed event - // is not persisted, the event itself needs to be persisted even though it hasn't been - // emitted yet. These are removed after the events are written. + // be failed upon reload. However, as the necessary information for the SpliceFailed and + // DiscardFunding events is not persisted, the events need to be persisted even though they + // haven't been emitted yet. These are removed after the events are written. let mut events = self.pending_events.lock().unwrap(); let event_count = events.len(); for peer_state in peer_states.iter() { @@ -17386,8 +17645,16 @@ impl< user_channel_id: chan.context.get_user_id(), abandoned_funding_txo: splice_funding_failed.funding_txo, channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + }, + None, + )); + events.push_back(( + events::Event::DiscardFunding { + channel_id: chan.context().channel_id(), + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, }, None, )); @@ -17510,7 +17777,7 @@ impl< (21, WithoutLength(&self.flow.writeable_async_receive_offer_cache()), required), }); - // Remove the SpliceFailed events added earlier. + // Remove the SpliceFailed and DiscardFunding events added earlier. events.truncate(event_count); Ok(()) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 66a0147e131..fe01d627965 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -14,13 +14,11 @@ use crate::blinded_path::payment::DummyTlvs; use crate::chain::channelmonitor::ChannelMonitor; use crate::chain::transaction::OutPoint; use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; -use crate::events::bump_transaction::sync::{ - BumpTransactionEventHandlerSync, WalletSourceSync, WalletSync, -}; +use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync; use crate::events::bump_transaction::BumpTransactionEvent; use crate::events::{ - ClaimedHTLC, ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PathFailure, - PaymentFailureReason, PaymentPurpose, + ClaimedHTLC, ClosureReason, Event, FundingInfo, HTLCHandlingFailureType, PaidBolt12Invoice, + PathFailure, PaymentFailureReason, PaymentPurpose, }; use crate::ln::chan_utils::{ commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, TRUC_MAX_WEIGHT, @@ -29,7 +27,7 @@ use crate::ln::channelmanager::{ AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RAACommitmentOrder, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::funding::FundingTxInput; +use crate::ln::funding::{FundingContribution, FundingTxInput}; use crate::ln::msgs::{self, OpenChannel}; use crate::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, RoutingMessageHandler, @@ -54,6 +52,7 @@ use crate::util::test_channel_signer::SignerOp; use crate::util::test_channel_signer::TestChannelSigner; use crate::util::test_utils::{self, TestLogger}; use crate::util::test_utils::{TestChainMonitor, TestKeysInterface, TestScorer}; +use crate::util::wallet_utils::{WalletSourceSync, WalletSync}; use bitcoin::amount::Amount; use bitcoin::block::{Block, Header, Version as BlockVersion}; @@ -3264,6 +3263,57 @@ pub fn expect_splice_pending_event<'a, 'b, 'c, 'd>( } } +#[cfg(any(test, ldk_bench, feature = "_test_utils"))] +pub fn expect_splice_failed_events<'a, 'b, 'c, 'd>( + node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId, + funding_contribution: FundingContribution, +) { + let events = node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + match &events[0] { + Event::SpliceFailed { channel_id, .. } => { + assert_eq!(*expected_channel_id, *channel_id); + }, + _ => panic!("Unexpected event"), + } + match &events[1] { + Event::DiscardFunding { funding_info, .. } => { + if let FundingInfo::Contribution { inputs, outputs } = &funding_info { + let (expected_inputs, expected_outputs) = + funding_contribution.into_contributed_inputs_and_outputs(); + assert_eq!(*inputs, expected_inputs); + assert_eq!(*outputs, expected_outputs); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Unexpected event"), + } +} + +#[cfg(any(test, ldk_bench, feature = "_test_utils"))] +pub fn expect_discard_funding_event<'a, 'b, 'c, 'd>( + node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId, + funding_contribution: FundingContribution, +) { + let events = node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::DiscardFunding { channel_id, funding_info } => { + assert_eq!(*expected_channel_id, *channel_id); + if let FundingInfo::Contribution { inputs, outputs } = &funding_info { + let (expected_inputs, expected_outputs) = + funding_contribution.into_contributed_inputs_and_outputs(); + assert_eq!(*inputs, expected_inputs); + assert_eq!(*outputs, expected_outputs); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Unexpected event"), + } +} + pub fn expect_probe_successful_events( node: &Node, mut probe_results: Vec<(PaymentHash, PaymentId)>, ) { diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 06b972d7126..16b8997479f 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -12,14 +12,9 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use bitcoin::{ - Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, SignedAmount, Transaction, TxOut, - WScriptHash, Weight, + Amount, FeeRate, OutPoint, ScriptBuf, SignedAmount, TxOut, WPubkeyHash, WScriptHash, Weight, }; -use core::ops::Deref; - -use crate::events::bump_transaction::sync::CoinSelectionSourceSync; -use crate::events::bump_transaction::{CoinSelection, CoinSelectionSource, Input, Utxo}; use crate::ln::chan_utils::{ make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, @@ -29,8 +24,10 @@ use crate::ln::msgs; use crate::ln::types::ChannelId; use crate::ln::LN_MAX_MSG_LEN; use crate::prelude::*; -use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; use crate::util::async_poll::MaybeSend; +use crate::util::wallet_utils::{ + CoinSelection, CoinSelectionSource, CoinSelectionSourceSync, Input, +}; /// A template for contributing to a channel's splice funding transaction. /// @@ -49,28 +46,38 @@ pub struct FundingTemplate { /// The fee rate to use for coin selection. feerate: FeeRate, - - /// Whether the contributor initiated the funding, and thus is responsible for fees incurred for - /// common fields and shared inputs and outputs. - is_initiator: bool, } impl FundingTemplate { /// Constructs a [`FundingTemplate`] for a splice using the provided shared input. - pub(super) fn new(shared_input: Option, feerate: FeeRate, is_initiator: bool) -> Self { - Self { shared_input, feerate, is_initiator } + pub(super) fn new(shared_input: Option, feerate: FeeRate) -> Self { + Self { shared_input, feerate } } } macro_rules! build_funding_contribution { - ($value_added:expr, $outputs:expr, $shared_input:expr, $feerate:expr, $is_initiator:expr, $wallet:ident, $($await:tt)*) => {{ + ($value_added:expr, $outputs:expr, $shared_input:expr, $feerate:expr, $wallet:ident, $($await:tt)*) => {{ let value_added: Amount = $value_added; let outputs: Vec = $outputs; let shared_input: Option = $shared_input; let feerate: FeeRate = $feerate; - let is_initiator: bool = $is_initiator; - let value_removed = outputs.iter().map(|txout| txout.value).sum(); + // Validate user-provided amounts are within MAX_MONEY before coin selection to + // ensure FundingContribution::net_value() arithmetic cannot overflow. With all + // amounts bounded by MAX_MONEY (~2.1e15 sat), the worst-case net_value() + // computation is -2 * MAX_MONEY (~-4.2e15), well within i64::MIN (~-9.2e18). + if value_added > Amount::MAX_MONEY { + return Err(()); + } + + let mut value_removed = Amount::ZERO; + for txout in outputs.iter() { + value_removed = match value_removed.checked_add(txout.value) { + Some(sum) if sum <= Amount::MAX_MONEY => sum, + _ => return Err(()), + }; + } + let is_splice = shared_input.is_some(); let coin_selection = if value_added == Amount::ZERO { @@ -108,7 +115,11 @@ macro_rules! build_funding_contribution { let CoinSelection { confirmed_utxos: inputs, change_output } = coin_selection; - let estimated_fee = estimate_transaction_fee(&inputs, &outputs, is_initiator, is_splice, feerate); + // The caller creating a FundingContribution is always the initiator for fee estimation + // purposes — this is conservative, overestimating rather than underestimating fees if + // the node ends up as the acceptor. + let estimated_fee = estimate_transaction_fee(&inputs, &outputs, true, is_splice, feerate); + debug_assert!(estimated_fee <= Amount::MAX_MONEY); let contribution = FundingContribution { value_added, @@ -117,7 +128,6 @@ macro_rules! build_funding_contribution { outputs, change_output, feerate, - is_initiator, is_splice, }; @@ -128,113 +138,74 @@ macro_rules! build_funding_contribution { impl FundingTemplate { /// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform /// coin selection. - pub async fn splice_in( + pub async fn splice_in( self, value_added: Amount, wallet: W, - ) -> Result - where - W::Target: CoinSelectionSource + MaybeSend, - { + ) -> Result { if value_added == Amount::ZERO { return Err(()); } - let FundingTemplate { shared_input, feerate, is_initiator } = self; - build_funding_contribution!(value_added, vec![], shared_input, feerate, is_initiator, wallet, await) + let FundingTemplate { shared_input, feerate } = self; + build_funding_contribution!(value_added, vec![], shared_input, feerate, wallet, await) } /// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform /// coin selection. - pub fn splice_in_sync( + pub fn splice_in_sync( self, value_added: Amount, wallet: W, - ) -> Result - where - W::Target: CoinSelectionSourceSync, - { + ) -> Result { if value_added == Amount::ZERO { return Err(()); } - let FundingTemplate { shared_input, feerate, is_initiator } = self; - build_funding_contribution!( - value_added, - vec![], - shared_input, - feerate, - is_initiator, - wallet, - ) + let FundingTemplate { shared_input, feerate } = self; + build_funding_contribution!(value_added, vec![], shared_input, feerate, wallet,) } /// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to /// perform coin selection. - pub async fn splice_out( + pub async fn splice_out( self, outputs: Vec, wallet: W, - ) -> Result - where - W::Target: CoinSelectionSource + MaybeSend, - { + ) -> Result { if outputs.is_empty() { return Err(()); } - let FundingTemplate { shared_input, feerate, is_initiator } = self; - build_funding_contribution!(Amount::ZERO, outputs, shared_input, feerate, is_initiator, wallet, await) + let FundingTemplate { shared_input, feerate } = self; + build_funding_contribution!(Amount::ZERO, outputs, shared_input, feerate, wallet, await) } /// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to /// perform coin selection. - pub fn splice_out_sync( + pub fn splice_out_sync( self, outputs: Vec, wallet: W, - ) -> Result - where - W::Target: CoinSelectionSourceSync, - { + ) -> Result { if outputs.is_empty() { return Err(()); } - let FundingTemplate { shared_input, feerate, is_initiator } = self; - build_funding_contribution!( - Amount::ZERO, - outputs, - shared_input, - feerate, - is_initiator, - wallet, - ) + let FundingTemplate { shared_input, feerate } = self; + build_funding_contribution!(Amount::ZERO, outputs, shared_input, feerate, wallet,) } /// Creates a [`FundingContribution`] for both adding and removing funds from a channel using /// `wallet` to perform coin selection. - pub async fn splice_in_and_out( + pub async fn splice_in_and_out( self, value_added: Amount, outputs: Vec, wallet: W, - ) -> Result - where - W::Target: CoinSelectionSource + MaybeSend, - { + ) -> Result { if value_added == Amount::ZERO && outputs.is_empty() { return Err(()); } - let FundingTemplate { shared_input, feerate, is_initiator } = self; - build_funding_contribution!(value_added, outputs, shared_input, feerate, is_initiator, wallet, await) + let FundingTemplate { shared_input, feerate } = self; + build_funding_contribution!(value_added, outputs, shared_input, feerate, wallet, await) } /// Creates a [`FundingContribution`] for both adding and removing funds from a channel using /// `wallet` to perform coin selection. - pub fn splice_in_and_out_sync( + pub fn splice_in_and_out_sync( self, value_added: Amount, outputs: Vec, wallet: W, - ) -> Result - where - W::Target: CoinSelectionSourceSync, - { + ) -> Result { if value_added == Amount::ZERO && outputs.is_empty() { return Err(()); } - let FundingTemplate { shared_input, feerate, is_initiator } = self; - build_funding_contribution!( - value_added, - outputs, - shared_input, - feerate, - is_initiator, - wallet, - ) + let FundingTemplate { shared_input, feerate } = self; + build_funding_contribution!(value_added, outputs, shared_input, feerate, wallet,) } } @@ -313,10 +284,6 @@ pub struct FundingContribution { /// The fee rate used to select `inputs`. feerate: FeeRate, - /// Whether the contributor initiated the funding, and thus is responsible for fees incurred for - /// common fields and shared inputs and outputs. - is_initiator: bool, - /// Whether the contribution is for funding a splice. is_splice: bool, } @@ -328,8 +295,7 @@ impl_writeable_tlv_based!(FundingContribution, { (7, outputs, optional_vec), (9, change_output, option), (11, feerate, required), - (13, is_initiator, required), - (15, is_splice, required), + (13, is_splice, required), }); impl FundingContribution { @@ -337,14 +303,18 @@ impl FundingContribution { self.feerate } - pub(super) fn is_initiator(&self) -> bool { - self.is_initiator - } - pub(super) fn is_splice(&self) -> bool { self.is_splice } + pub(super) fn contributed_inputs(&self) -> impl Iterator + '_ { + self.inputs.iter().map(|input| input.utxo.outpoint) + } + + pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { + self.outputs.iter().chain(self.change_output.iter()) + } + pub(super) fn into_tx_parts(self) -> (Vec, Vec) { let FundingContribution { inputs, mut outputs, change_output, .. } = self; @@ -361,10 +331,9 @@ impl FundingContribution { (inputs.into_iter().map(|input| input.utxo.outpoint).collect(), outputs) } - /// The net value contributed to a channel by the splice. If negative, more value will be - /// spliced out than spliced in. Fees will be deducted from the expected splice-out amount - /// if no inputs were included. - pub fn net_value(&self) -> Result { + /// Validates that the funding inputs are suitable for use in the interactive transaction + /// protocol, checking prevtx sizes and input sufficiency. + pub fn validate(&self) -> Result<(), String> { for FundingTxInput { utxo, prevtx, .. } in self.inputs.iter() { use crate::util::ser::Writeable; const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { @@ -417,204 +386,180 @@ impl FundingContribution { } } - let unpaid_fees = if self.inputs.is_empty() { self.estimated_fee } else { Amount::ZERO } - .to_signed() - .expect("fees should never exceed Amount::MAX_MONEY"); - let value_added = self.value_added.to_signed().map_err(|_| "Value added too large")?; - let value_removed = self - .outputs - .iter() - .map(|txout| txout.value) - .sum::() - .to_signed() - .map_err(|_| "Value removed too large")?; - - let contribution_amount = value_added - value_removed; - 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) + Ok(()) } -} -/// An input to contribute to a channel's funding transaction either when using the v2 channel -/// establishment protocol or when splicing. -#[derive(Debug, Clone)] -pub struct FundingTxInput { - /// The unspent [`TxOut`] found in [`prevtx`]. - /// - /// [`TxOut`]: bitcoin::TxOut - /// [`prevtx`]: Self::prevtx - pub(crate) utxo: Utxo, - - /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. + /// Computes the feerate adjustment as a pure `&self` operation, returning the new estimated + /// fee and optionally the new change output value. /// - /// [`TxOut`]: bitcoin::TxOut - /// [`utxo`]: Self::utxo - pub(crate) prevtx: Transaction, -} + /// Returns `Ok((new_estimated_fee, new_change_value))` or `Err`: + /// - `(fee, Some(change))` — inputs with change: both should be updated + /// - `(fee, None)` — inputs without change (or change removed), or splice-out: fee updated + /// only + fn compute_feerate_adjustment( + &self, target_feerate: FeeRate, + ) -> Result<(Amount, Option), String> { + let is_splice = self.is_splice; -impl_writeable_tlv_based!(FundingTxInput, { - (1, utxo, required), - (3, _sequence, (legacy, Sequence, - |read_val: Option<&Sequence>| { - if let Some(sequence) = read_val { - // Utxo contains sequence now, so update it if the value read here differs since - // this indicates Utxo::sequence was read with default_value - let utxo: &mut Utxo = utxo.0.as_mut().expect("utxo is required"); - if utxo.sequence != *sequence { - utxo.sequence = *sequence; + if !self.inputs.is_empty() { + let budget = self.estimated_fee; + + if let Some(ref change_output) = self.change_output { + let old_change_value = change_output.value; + let dust_limit = change_output.script_pubkey.minimal_non_dust(); + + // Fair fee including the change output's weight. + let all_outputs: Vec = + self.outputs.iter().chain(self.change_output.iter()).cloned().collect(); + let fair_fee = estimate_transaction_fee( + &self.inputs, + &all_outputs, + false, + is_splice, + target_feerate, + ); + + let available = budget + .checked_add(old_change_value) + .ok_or("Budget plus change value overflow".to_string())?; + + match available.checked_sub(fair_fee) { + Some(new_change_value) if new_change_value >= dust_limit => { + Ok((fair_fee, Some(new_change_value))) + }, + _ => { + // Change would be below dust or negative. Try without change. + let fair_fee_no_change = estimate_transaction_fee( + &self.inputs, + &self.outputs, + false, + is_splice, + target_feerate, + ); + if available >= fair_fee_no_change { + Ok((fair_fee_no_change, None)) + } else { + Err(format!( + "Feerate too high: available fee budget {} insufficient for required fee {}", + available, fair_fee_no_change, + )) + } + }, } - } - Ok(()) - }, - |input: &FundingTxInput| Some(input.utxo.sequence))), - (5, prevtx, required), -}); - -impl FundingTxInput { - fn new bool>( - prevtx: Transaction, vout: u32, witness_weight: Weight, script_filter: F, - ) -> Result { - Ok(FundingTxInput { - utxo: Utxo { - outpoint: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout }, - output: prevtx - .output - .get(vout as usize) - .filter(|output| script_filter(&output.script_pubkey)) - .ok_or(())? - .clone(), - satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - }, - prevtx, - }) - } - - /// Creates an input spending a P2WPKH output from the given `prevtx` at index `vout`. - /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. - /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. - /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2wpkh(prevtx: Transaction, vout: u32) -> Result { - let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT) - - if cfg!(feature = "grind_signatures") { - // Guarantees a low R signature - Weight::from_wu(1) } else { - Weight::ZERO - }; - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wpkh) - } - - /// Creates an input spending a P2WSH output from the given `prevtx` at index `vout`. - /// - /// Requires passing the weight of witness needed to satisfy the output's script. - /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. - /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. - /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2wsh(prevtx: Transaction, vout: u32, witness_weight: Weight) -> Result { - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wsh) - } - - /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. - /// - /// This is meant for inputs spending a taproot output using the key path. See - /// [`new_p2tr_script_spend`] for when spending using a script path. - /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. - /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. - /// - /// [`new_p2tr_script_spend`]: Self::new_p2tr_script_spend - /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2tr_key_spend(prevtx: Transaction, vout: u32) -> Result { - let witness_weight = Weight::from_wu(P2TR_KEY_PATH_WITNESS_WEIGHT); - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2tr) + // No change output. + let fair_fee = estimate_transaction_fee( + &self.inputs, + &self.outputs, + false, + is_splice, + target_feerate, + ); + if budget < fair_fee { + return Err(format!( + "Feerate too high: fee budget {} insufficient for required fee {}", + budget, fair_fee, + )); + } + let surplus = budget - fair_fee; + let dust_limit = + ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()).minimal_non_dust(); + if surplus >= dust_limit { + return Err(format!( + "Fee surplus {} exceeds dust limit {}; cannot burn without change output", + surplus, dust_limit, + )); + } + Ok((fair_fee, None)) + } + } else { + // No inputs (splice-out): fees paid from channel balance. + let fair_fee = + estimate_transaction_fee(&[], &self.outputs, false, is_splice, target_feerate); + if self.estimated_fee < fair_fee { + return Err(format!( + "Feerate too high: estimated fee {} insufficient for required fee {}", + self.estimated_fee, fair_fee, + )); + } + // Surplus goes back to the channel balance. + Ok((fair_fee, None)) + } } - /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. - /// - /// Requires passing the weight of witness needed to satisfy a script path of the taproot - /// output. See [`new_p2tr_key_spend`] for when spending using the key path. - /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. + /// Adjusts the contribution's change output for the initiator's feerate. /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. - /// - /// [`new_p2tr_key_spend`]: Self::new_p2tr_key_spend - /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2tr_script_spend( - prevtx: Transaction, vout: u32, witness_weight: Weight, - ) -> Result { - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2tr) - } - - #[cfg(test)] - pub(crate) fn new_p2pkh(prevtx: Transaction, vout: u32) -> Result { - FundingTxInput::new(prevtx, vout, Weight::ZERO, Script::is_p2pkh) - } - - /// The outpoint of the UTXO being spent. - pub fn outpoint(&self) -> bitcoin::OutPoint { - self.utxo.outpoint - } - - /// The unspent output. - pub fn output(&self) -> &TxOut { - &self.utxo.output + /// When the acceptor has a pending contribution (from the quiescence tie-breaker scenario), + /// the initiator's proposed feerate may differ from the feerate used during coin selection. + /// This adjusts the change output so the acceptor pays their fair share at the target + /// feerate. + pub(super) fn adjust_for_feerate(&mut self, target_feerate: FeeRate) -> Result<(), String> { + let (new_estimated_fee, new_change) = self.compute_feerate_adjustment(target_feerate)?; + match new_change { + Some(value) => self.change_output.as_mut().unwrap().value = value, + None => self.change_output = None, + } + self.estimated_fee = new_estimated_fee; + self.feerate = target_feerate; + Ok(()) } - /// The sequence number to use in the [`TxIn`]. + /// Returns the net value at the given target feerate without mutating `self`. /// - /// [`TxIn`]: bitcoin::TxIn - pub fn sequence(&self) -> Sequence { - self.utxo.sequence + /// This serves double duty: it checks feerate compatibility (returning `Err` if the feerate + /// can't be accommodated) and computes the adjusted net value (returning `Ok` with the value + /// accounting for the target feerate). + pub(super) fn net_value_at_feerate( + &self, target_feerate: FeeRate, + ) -> Result { + let (new_estimated_fee, _) = self.compute_feerate_adjustment(target_feerate)?; + Ok(self.net_value_with_fee(new_estimated_fee)) } - /// Sets the sequence number to use in the [`TxIn`]. - /// - /// [`TxIn`]: bitcoin::TxIn - pub fn set_sequence(&mut self, sequence: Sequence) { - self.utxo.sequence = sequence; + /// The net value contributed to a channel by the splice. If negative, more value will be + /// spliced out than spliced in. Fees will be deducted from the expected splice-out amount + /// if no inputs were included. + pub fn net_value(&self) -> SignedAmount { + self.net_value_with_fee(self.estimated_fee) } - /// Converts the [`FundingTxInput`] into a [`Utxo`]. - pub fn into_utxo(self) -> Utxo { - self.utxo - } + /// Computes the net value using the given `estimated_fee` for the splice-out (no inputs) + /// case. For splice-in, fees are paid by inputs so `estimated_fee` is not deducted. + fn net_value_with_fee(&self, estimated_fee: Amount) -> SignedAmount { + let unpaid_fees = if self.inputs.is_empty() { estimated_fee } else { Amount::ZERO } + .to_signed() + .expect("estimated_fee is validated to not exceed Amount::MAX_MONEY"); + let value_added = self + .value_added + .to_signed() + .expect("value_added is validated to not exceed Amount::MAX_MONEY"); + let value_removed = self + .outputs + .iter() + .map(|txout| txout.value) + .sum::() + .to_signed() + .expect("value_removed is validated to not exceed Amount::MAX_MONEY"); - /// Converts the [`FundingTxInput`] into a [`TxOut`]. - pub fn into_output(self) -> TxOut { - self.utxo.output + let contribution_amount = value_added - value_removed; + contribution_amount + .checked_sub(unpaid_fees) + .expect("all amounts are validated to not exceed Amount::MAX_MONEY") } } +/// An input to contribute to a channel's funding transaction either when using the v2 channel +/// establishment protocol or when splicing. +pub type FundingTxInput = crate::util::wallet_utils::ConfirmedUtxo; + #[cfg(test)] mod tests { - use super::{estimate_transaction_fee, FundingContribution, FundingTxInput}; + use super::{estimate_transaction_fee, FundingContribution, FundingTemplate, FundingTxInput}; + use crate::chain::ClaimId; + use crate::util::wallet_utils::{CoinSelection, CoinSelectionSourceSync, Input}; use bitcoin::hashes::Hash; use bitcoin::transaction::{Transaction, TxOut, Version}; - use bitcoin::{Amount, FeeRate, ScriptBuf, SignedAmount, WPubkeyHash}; + use bitcoin::{Amount, FeeRate, Psbt, ScriptBuf, SignedAmount, WPubkeyHash}; #[test] #[rustfmt::skip] @@ -701,11 +646,11 @@ mod tests { ], outputs: vec![], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(2000), }; - assert_eq!(contribution.net_value(), Ok(contribution.value_added.to_signed().unwrap())); + assert!(contribution.validate().is_ok()); + assert_eq!(contribution.net_value(), contribution.value_added.to_signed().unwrap()); } // Net splice-in @@ -722,11 +667,11 @@ mod tests { funding_output_sats(200_000), ], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(2000), }; - assert_eq!(contribution.net_value(), Ok(SignedAmount::from_sat(220_000 - 200_000))); + assert!(contribution.validate().is_ok()); + assert_eq!(contribution.net_value(), SignedAmount::from_sat(220_000 - 200_000)); } // Net splice-out @@ -743,11 +688,11 @@ mod tests { funding_output_sats(400_000), ], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(2000), }; - assert_eq!(contribution.net_value(), Ok(SignedAmount::from_sat(220_000 - 400_000))); + assert!(contribution.validate().is_ok()); + assert_eq!(contribution.net_value(), SignedAmount::from_sat(220_000 - 400_000)); } // Net splice-out, inputs insufficient to cover fees @@ -764,12 +709,11 @@ mod tests { funding_output_sats(400_000), ], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(90000), }; assert_eq!( - contribution.net_value(), + 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), @@ -788,12 +732,11 @@ mod tests { ], outputs: vec![], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(2000), }; assert_eq!( - contribution.net_value(), + 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), @@ -813,11 +756,11 @@ mod tests { ], outputs: vec![], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(2000), }; - assert_eq!(contribution.net_value(), Ok(contribution.value_added.to_signed().unwrap())); + assert!(contribution.validate().is_ok()); + assert_eq!(contribution.net_value(), contribution.value_added.to_signed().unwrap()); } // higher fee rate, does not cover @@ -832,12 +775,11 @@ mod tests { ], outputs: vec![], change_output: None, - is_initiator: true, is_splice: true, feerate: FeeRate::from_sat_per_kwu(2200), }; assert_eq!( - contribution.net_value(), + 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), @@ -845,9 +787,9 @@ mod tests { ); } - // barely covers, less fees (no extra weight, not initiator) + // barely covers, less fees (not a splice) { - let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; + let expected_fee = if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }; let contribution = FundingContribution { value_added: Amount::from_sat(300_000 - expected_fee - 20), estimated_fee: Amount::from_sat(expected_fee), @@ -857,11 +799,371 @@ mod tests { ], outputs: vec![], change_output: None, - is_initiator: false, is_splice: false, feerate: FeeRate::from_sat_per_kwu(2000), }; - assert_eq!(contribution.net_value(), Ok(contribution.value_added.to_signed().unwrap())); + assert!(contribution.validate().is_ok()); + assert_eq!(contribution.net_value(), contribution.value_added.to_signed().unwrap()); + } + } + + struct UnreachableWallet; + + impl CoinSelectionSourceSync for UnreachableWallet { + fn select_confirmed_utxos( + &self, _claim_id: Option, _must_spend: Vec, _must_pay_to: &[TxOut], + _target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, + ) -> Result { + unreachable!("should not reach coin selection") + } + fn sign_psbt(&self, _psbt: Psbt) -> Result { + unreachable!("should not reach signing") + } + } + + #[test] + fn test_build_funding_contribution_validates_max_money() { + let over_max = Amount::MAX_MONEY + Amount::from_sat(1); + let feerate = FeeRate::from_sat_per_kwu(2000); + + // splice_in_sync with value_added > MAX_MONEY + { + let template = FundingTemplate::new(None, feerate); + assert!(template.splice_in_sync(over_max, UnreachableWallet).is_err()); + } + + // splice_out_sync with single output value > MAX_MONEY + { + let template = FundingTemplate::new(None, feerate); + let outputs = vec![funding_output_sats(over_max.to_sat())]; + assert!(template.splice_out_sync(outputs, UnreachableWallet).is_err()); + } + + // splice_out_sync with multiple outputs summing > MAX_MONEY + { + let template = FundingTemplate::new(None, feerate); + let half_over = Amount::MAX_MONEY / 2 + Amount::from_sat(1); + let outputs = vec![ + funding_output_sats(half_over.to_sat()), + funding_output_sats(half_over.to_sat()), + ]; + assert!(template.splice_out_sync(outputs, UnreachableWallet).is_err()); + } + + // splice_in_and_out_sync with value_added > MAX_MONEY + { + let template = FundingTemplate::new(None, feerate); + let outputs = vec![funding_output_sats(1_000)]; + assert!(template.splice_in_and_out_sync(over_max, outputs, UnreachableWallet).is_err()); } + + // splice_in_and_out_sync with output sum > MAX_MONEY + { + let template = FundingTemplate::new(None, feerate); + let outputs = vec![funding_output_sats(over_max.to_sat())]; + assert!(template + .splice_in_and_out_sync(Amount::from_sat(1_000), outputs, UnreachableWallet) + .is_err()); + } + } + + #[test] + fn test_adjust_for_feerate_higher_change_adjusted() { + // Splice-in: higher target feerate reduces the change output. + // The budget (is_initiator=true) overestimates by including common TX fields, + // shared output, and shared input weight. So we need a sufficiently high target + // feerate for the acceptor's fair fee to exceed the budget, causing the change + // to decrease. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(5000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(10_000); + + // Budget computed as initiator (overestimate, without change output weight). + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let mut contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input.clone()], + outputs: vec![], + change_output: Some(change.clone()), + feerate: original_feerate, + is_splice: true, + }; + + let net_value_before = contribution.net_value(); + assert!(contribution.adjust_for_feerate(target_feerate).is_ok()); + + // Fair fee at target feerate for acceptor (is_initiator=false), including change weight. + let expected_fair_fee = + estimate_transaction_fee(&[input], &[change], false, true, target_feerate); + let expected_change = budget + Amount::from_sat(10_000) - expected_fair_fee; + + assert_eq!(contribution.estimated_fee, expected_fair_fee); + assert!(contribution.change_output.is_some()); + assert_eq!(contribution.change_output.as_ref().unwrap().value, expected_change); + assert!(expected_change < Amount::from_sat(10_000)); // Change reduced + assert_eq!(contribution.net_value(), net_value_before); + } + + #[test] + fn test_adjust_for_feerate_lower_change_increased() { + // Splice-in: lower target feerate increases the change output. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(1000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(10_000); + + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let mut contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input.clone()], + outputs: vec![], + change_output: Some(change.clone()), + feerate: original_feerate, + is_splice: true, + }; + + let net_value_before = contribution.net_value(); + assert!(contribution.adjust_for_feerate(target_feerate).is_ok()); + + let expected_fair_fee = + estimate_transaction_fee(&[input], &[change], false, true, target_feerate); + let expected_change = budget + Amount::from_sat(10_000) - expected_fair_fee; + + assert_eq!(contribution.estimated_fee, expected_fair_fee); + assert!(contribution.change_output.is_some()); + assert_eq!(contribution.change_output.as_ref().unwrap().value, expected_change); + assert!(expected_change > Amount::from_sat(10_000)); // Change increased + assert_eq!(contribution.net_value(), net_value_before); + } + + #[test] + fn test_adjust_for_feerate_change_removed() { + // Splice-in: feerate high enough that change drops below dust and is removed, + // but budget + change still covers the fee without the change output. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(7000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(500); + + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let mut contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input.clone()], + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + is_splice: true, + }; + + let net_value_before = contribution.net_value(); + assert!(contribution.adjust_for_feerate(target_feerate).is_ok()); + + // Change should be removed; estimated_fee updated to no-change fair fee. + assert!(contribution.change_output.is_none()); + let expected_fee_no_change = + estimate_transaction_fee(&[input], &[], false, true, target_feerate); + assert_eq!(contribution.estimated_fee, expected_fee_no_change); + assert_eq!(contribution.net_value(), net_value_before); + } + + #[test] + fn test_adjust_for_feerate_too_high_rejected() { + // Splice-in: feerate so high that even without change, the fee can't be covered. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(500); + + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let mut contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input], + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + is_splice: true, + }; + + let result = contribution.adjust_for_feerate(target_feerate); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Feerate too high")); + } + + #[test] + fn test_adjust_for_feerate_splice_out_sufficient() { + // Splice-out (no inputs): budget from is_initiator=true overestimate covers the + // acceptor's fair fee at a moderately higher target feerate. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let output = funding_output_sats(50_000); + + let budget = estimate_transaction_fee(&[], &[output.clone()], true, true, original_feerate); + + let mut contribution = FundingContribution { + value_added: Amount::ZERO, + estimated_fee: budget, + inputs: vec![], + outputs: vec![output.clone()], + change_output: None, + feerate: original_feerate, + is_splice: true, + }; + + assert!(contribution.adjust_for_feerate(target_feerate).is_ok()); + // estimated_fee is updated to the fair fee; surplus goes back to channel balance. + let expected_fair_fee = + estimate_transaction_fee(&[], &[output], false, true, target_feerate); + assert_eq!(contribution.estimated_fee, expected_fair_fee); + assert!(expected_fair_fee <= budget); + } + + #[test] + fn test_adjust_for_feerate_splice_out_insufficient() { + // Splice-out: target feerate too high for the is_initiator=true budget. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(50_000); + let output = funding_output_sats(50_000); + + let budget = estimate_transaction_fee(&[], &[output.clone()], true, true, original_feerate); + + let mut contribution = FundingContribution { + value_added: Amount::ZERO, + estimated_fee: budget, + inputs: vec![], + outputs: vec![output], + change_output: None, + feerate: original_feerate, + is_splice: true, + }; + + let result = contribution.adjust_for_feerate(target_feerate); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Feerate too high")); + } + + #[test] + fn test_net_value_at_feerate_splice_in() { + // Splice-in: net_value_at_feerate returns the same value as net_value() since + // splice-in fees are paid by inputs, not from channel balance. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(10_000); + + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input], + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + is_splice: true, + }; + + // For splice-in, unpaid_fees is zero so net_value_at_feerate equals net_value. + let net_at_feerate = contribution.net_value_at_feerate(target_feerate).unwrap(); + assert_eq!(net_at_feerate, contribution.net_value()); + assert_eq!(net_at_feerate, Amount::from_sat(50_000).to_signed().unwrap()); + } + + #[test] + fn test_net_value_at_feerate_splice_out() { + // Splice-out: net_value_at_feerate returns the adjusted value using the fair fee + // at the target feerate. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let output = funding_output_sats(50_000); + + let budget = estimate_transaction_fee(&[], &[output.clone()], true, true, original_feerate); + + let contribution = FundingContribution { + value_added: Amount::ZERO, + estimated_fee: budget, + inputs: vec![], + outputs: vec![output.clone()], + change_output: None, + feerate: original_feerate, + is_splice: true, + }; + + let net_at_feerate = contribution.net_value_at_feerate(target_feerate).unwrap(); + + // The fair fee at target feerate should be less than the initiator's budget. + let fair_fee = estimate_transaction_fee(&[], &[output], false, true, target_feerate); + let expected_net = SignedAmount::ZERO + - Amount::from_sat(50_000).to_signed().unwrap() + - fair_fee.to_signed().unwrap(); + assert_eq!(net_at_feerate, expected_net); + + // Should be less negative than net_value() which uses the higher budget. + assert!(net_at_feerate > contribution.net_value()); + } + + #[test] + fn test_net_value_at_feerate_does_not_mutate() { + // Verify net_value_at_feerate does not modify the contribution. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(5000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(10_000); + + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input], + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + is_splice: true, + }; + + let net_before = contribution.net_value(); + let fee_before = contribution.estimated_fee; + let change_before = contribution.change_output.as_ref().unwrap().value; + + let _ = contribution.net_value_at_feerate(target_feerate); + + // Nothing should have changed. + assert_eq!(contribution.net_value(), net_before); + assert_eq!(contribution.estimated_fee, fee_before); + assert_eq!(contribution.change_output.as_ref().unwrap().value, change_before); + } + + #[test] + fn test_net_value_at_feerate_too_high() { + // net_value_at_feerate returns Err when feerate can't be accommodated. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let input = funding_input_sats(100_000); + let change = funding_output_sats(500); + + let budget = estimate_transaction_fee(&[input.clone()], &[], true, true, original_feerate); + + let contribution = FundingContribution { + value_added: Amount::from_sat(50_000), + estimated_fee: budget, + inputs: vec![input], + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + is_splice: true, + }; + + let result = contribution.net_value_at_feerate(target_feerate); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Feerate too high")); } } diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index c5db1bcbe8a..1bc8ca2e095 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -12,7 +12,7 @@ use crate::io_extras::sink; use crate::prelude::*; use bitcoin::absolute::LockTime as AbsoluteLockTime; -use bitcoin::amount::{Amount, SignedAmount}; +use bitcoin::amount::Amount; use bitcoin::consensus::Encodable; use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::ecdsa::Signature as BitcoinSignature; @@ -31,7 +31,7 @@ use crate::ln::chan_utils::{ BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, }; -use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; +use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::funding::FundingTxInput; use crate::ln::msgs; use crate::ln::msgs::{MessageSendEvent, SerialId, TxSignatures}; @@ -2012,7 +2012,6 @@ pub(super) struct InteractiveTxConstructorArgs<'a, ES: EntropySource> { pub counterparty_node_id: PublicKey, pub channel_id: ChannelId, pub feerate_sat_per_kw: u32, - pub is_initiator: bool, pub funding_tx_locktime: AbsoluteLockTime, pub inputs_to_contribute: Vec, pub shared_funding_input: Option, @@ -2023,18 +2022,15 @@ pub(super) struct InteractiveTxConstructorArgs<'a, ES: EntropySource> { impl InteractiveTxConstructor { /// Instantiates a new `InteractiveTxConstructor`. /// - /// If the holder is the initiator, they need to send the first message which is a `TxAddInput` - /// message. - pub fn new( - args: InteractiveTxConstructorArgs, - ) -> Result { + /// Use [`Self::new_for_outbound`] or [`Self::new_for_inbound`] instead to also prepare the + /// first message for the initiator. + fn new(args: InteractiveTxConstructorArgs, is_initiator: bool) -> Self { let InteractiveTxConstructorArgs { entropy_source, holder_node_id, counterparty_node_id, channel_id, feerate_sat_per_kw, - is_initiator, funding_tx_locktime, inputs_to_contribute, shared_funding_input, @@ -2104,7 +2100,7 @@ impl InteractiveTxConstructor { let next_input_index = (!inputs_to_contribute.is_empty()).then_some(0); let next_output_index = (!outputs_to_contribute.is_empty()).then_some(0); - let mut constructor = Self { + Self { state_machine, is_initiator, initiator_first_message: None, @@ -2113,19 +2109,32 @@ impl InteractiveTxConstructor { outputs_to_contribute, next_input_index, next_output_index, - }; - // We'll store the first message for the initiator. - if is_initiator { - match constructor.maybe_send_message() { - Ok(message) => { - constructor.initiator_first_message = Some(message); - }, - Err(reason) => { - return Err(constructor.into_negotiation_error(reason)); - }, - } } - Ok(constructor) + } + + /// Instantiates a new `InteractiveTxConstructor` for the initiator (outbound splice). + /// + /// The initiator always has the shared funding output added internally, so preparing the + /// first message should never fail. Debug asserts verify this invariant. + pub fn new_for_outbound(args: InteractiveTxConstructorArgs) -> Self { + let mut constructor = Self::new(args, true); + match constructor.maybe_send_message() { + Ok(message) => constructor.initiator_first_message = Some(message), + Err(reason) => { + debug_assert!( + false, + "Outbound constructor should always have inputs: {:?}", + reason + ); + }, + } + constructor + } + + /// Instantiates a new `InteractiveTxConstructor` for the non-initiator (inbound splice or + /// dual-funded channel acceptor). + pub fn new_for_inbound(args: InteractiveTxConstructorArgs) -> Self { + Self::new(args, false) } fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { @@ -2313,102 +2322,16 @@ impl InteractiveTxConstructor { } } -/// Determine whether a change output should be added, and if yes, of what size, considering our -/// given inputs and outputs, and intended contribution. Takes into account the fees and the dust -/// limit. -/// -/// Three outcomes are possible: -/// - Inputs are sufficient for intended contribution, fees, and a larger-than-dust change: -/// `Ok(Some(change_amount))` -/// - Inputs are sufficient for intended contribution and fees, and a change output isn't needed: -/// `Ok(None)` -/// - Inputs are not sufficient to cover contribution and fees: -/// `Err(AbortReason::InsufficientFees)` -/// -/// Parameters: -/// - `context` - Context of the funding negotiation, including non-shared inputs and feerate. -/// - `is_splice` - Whether we splicing an existing channel or dual-funding a new one. -/// - `shared_output_funding_script` - The script of the shared output. -/// - `funding_outputs` - Our funding outputs. -/// - `change_output_dust_limit` - The dust limit (in sats) to consider. -pub(super) fn calculate_change_output_value( - context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, - change_output_dust_limit: u64, -) -> 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_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_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()) - }); - let mut weight = our_funding_outputs_weight.saturating_add(our_funding_inputs_weight); - - // If we are the initiator, we must pay for the weight of the funding output and - // all common fields in the funding transaction. - if context.is_initiator { - weight = weight.saturating_add(get_output_weight(shared_output_funding_script).to_wu()); - weight = weight.saturating_add(TX_COMMON_FIELDS_WEIGHT); - if is_splice { - // TODO(taproot): Needs to consider different weights based on channel type - weight = weight.saturating_add(BASE_INPUT_WEIGHT); - weight = weight.saturating_add(EMPTY_SCRIPT_SIG_WEIGHT); - weight = weight.saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); - #[cfg(feature = "grind_signatures")] - { - // Guarantees a low R signature - weight -= 1; - } - } - } - - 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 = 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 { - // Enough to have over-dust change - Ok(Some(remaining_value)) - } -} - #[cfg(test)] mod tests { use crate::chain::chaininterface::{fee_for_weight, FEERATE_FLOOR_SATS_PER_KW}; - use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; + use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::funding::FundingTxInput; use crate::ln::interactivetxs::{ - calculate_change_output_value, generate_holder_serial_id, AbortReason, - HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, - InteractiveTxMessageSend, SharedOwnedInput, SharedOwnedOutput, MAX_INPUTS_OUTPUTS_COUNT, - MAX_RECEIVED_TX_ADD_INPUT_COUNT, MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, + generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, + InteractiveTxConstructorArgs, InteractiveTxMessageSend, SharedOwnedInput, + SharedOwnedOutput, MAX_INPUTS_OUTPUTS_COUNT, MAX_RECEIVED_TX_ADD_INPUT_COUNT, + MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, }; use crate::ln::types::ChannelId; use crate::sign::EntropySource; @@ -2423,8 +2346,7 @@ mod tests { use bitcoin::transaction::Version; use bitcoin::{opcodes, WScriptHash, Weight, XOnlyPublicKey}; use bitcoin::{ - OutPoint, PubkeyHash, ScriptBuf, Sequence, SignedAmount, Transaction, TxIn, TxOut, - WPubkeyHash, + OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, }; use super::{ @@ -2515,84 +2437,62 @@ mod tests { &SecretKey::from_slice(&[43; 32]).unwrap(), ); - let mut constructor_a = match InteractiveTxConstructor::new(InteractiveTxConstructorArgs { - entropy_source, - channel_id, - feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, - holder_node_id, - counterparty_node_id, - is_initiator: true, - funding_tx_locktime, - inputs_to_contribute: session.inputs_a, - shared_funding_input: session.a_shared_input.map(|(op, prev_output, lo)| { - SharedOwnedInput::new( - TxIn { - previous_output: op, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }, - prev_output, - lo, - true, // holder_sig_first - generate_funding_script_pubkey(), // witness_script for test - ) - }), - shared_funding_output: SharedOwnedOutput::new( - session.shared_output_a.0, - session.shared_output_a.1, - ), - outputs_to_contribute: session.outputs_a, - }) { - Ok(r) => Some(r), - Err(e) => { - assert_eq!( - Some((e.reason, ErrorCulprit::NodeA)), - session.expect_error, - "Test: {}", - session.description - ); - return; - }, - }; - let mut constructor_b = match InteractiveTxConstructor::new(InteractiveTxConstructorArgs { - entropy_source, - holder_node_id, - counterparty_node_id, - channel_id, - feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, - is_initiator: false, - funding_tx_locktime, - inputs_to_contribute: session.inputs_b, - shared_funding_input: session.b_shared_input.map(|(op, prev_output, lo)| { - SharedOwnedInput::new( - TxIn { - previous_output: op, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }, - prev_output, - lo, - false, // holder_sig_first - generate_funding_script_pubkey(), // witness_script for test - ) - }), - shared_funding_output: SharedOwnedOutput::new( - session.shared_output_b.0, - session.shared_output_b.1, - ), - outputs_to_contribute: session.outputs_b, - }) { - Ok(r) => Some(r), - Err(e) => { - assert_eq!( - Some((e.reason, ErrorCulprit::NodeB)), - session.expect_error, - "Test: {}", - session.description - ); - return; - }, - }; + let mut constructor_a = + Some(InteractiveTxConstructor::new_for_outbound(InteractiveTxConstructorArgs { + entropy_source, + channel_id, + feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, + holder_node_id, + counterparty_node_id, + funding_tx_locktime, + inputs_to_contribute: session.inputs_a, + shared_funding_input: session.a_shared_input.map(|(op, prev_output, lo)| { + SharedOwnedInput::new( + TxIn { + previous_output: op, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }, + prev_output, + lo, + true, // holder_sig_first + generate_funding_script_pubkey(), // witness_script for test + ) + }), + shared_funding_output: SharedOwnedOutput::new( + session.shared_output_a.0, + session.shared_output_a.1, + ), + outputs_to_contribute: session.outputs_a, + })); + let mut constructor_b = + Some(InteractiveTxConstructor::new_for_inbound(InteractiveTxConstructorArgs { + entropy_source, + holder_node_id, + counterparty_node_id, + channel_id, + feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, + funding_tx_locktime, + inputs_to_contribute: session.inputs_b, + shared_funding_input: session.b_shared_input.map(|(op, prev_output, lo)| { + SharedOwnedInput::new( + TxIn { + previous_output: op, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }, + prev_output, + lo, + false, // holder_sig_first + generate_funding_script_pubkey(), // witness_script for test + ) + }), + shared_funding_output: SharedOwnedOutput::new( + session.shared_output_b.0, + session.shared_output_b.1, + ), + outputs_to_contribute: session.outputs_b, + })); let handle_message_send = |msg: InteractiveTxMessageSend, for_constructor: &mut InteractiveTxConstructor| { @@ -3388,118 +3288,6 @@ mod tests { assert_eq!(generate_holder_serial_id(&&entropy_source, false) % 2, 1) } - #[test] - fn test_calculate_change_output_value_open() { - let input_prevouts = [ - TxOut { - value: Amount::from_sat(70_000), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - }, - TxOut { - value: Amount::from_sat(60_000), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - }, - ]; - let inputs = input_prevouts - .iter() - .map(|txout| { - let prevtx = Transaction { - input: Vec::new(), - output: vec![(*txout).clone()], - lock_time: AbsoluteLockTime::ZERO, - version: Version::TWO, - }; - - FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() - }) - .collect(); - let txout = TxOut { value: Amount::from_sat(10_000), script_pubkey: ScriptBuf::new() }; - let outputs = vec![txout]; - let funding_feerate_sat_per_1000_weight = 3000; - - let total_inputs: Amount = input_prevouts.iter().map(|o| o.value).sum(); - let total_outputs: Amount = outputs.iter().map(|o| o.value).sum(); - let fees = if cfg!(feature = "grind_signatures") { - Amount::from_sat(1734) - } else { - Amount::from_sat(1740) - }; - let common_fees = Amount::from_sat(234); - - // There is leftover for change - let context = FundingNegotiationContext { - is_initiator: true, - our_funding_contribution: SignedAmount::from_sat(110_000), - funding_tx_locktime: AbsoluteLockTime::ZERO, - funding_feerate_sat_per_1000_weight, - shared_funding_input: None, - our_funding_inputs: inputs, - our_funding_outputs: outputs, - }; - let gross_change = - 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)), - ); - - // 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)), - ); - - // Insufficient inputs, no leftover - let context = FundingNegotiationContext { - is_initiator: false, - our_funding_contribution: SignedAmount::from_sat(130_000), - ..context - }; - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Err(AbortReason::InsufficientFees), - ); - - // Very small leftover - let context = FundingNegotiationContext { - is_initiator: false, - our_funding_contribution: SignedAmount::from_sat(118_000), - ..context - }; - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(None), - ); - - // Small leftover, but not dust - let context = FundingNegotiationContext { - is_initiator: false, - our_funding_contribution: SignedAmount::from_sat(117_992), - ..context - }; - let gross_change = - 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)), - ); - - // Larger fee, smaller change - let context = FundingNegotiationContext { - is_initiator: true, - our_funding_contribution: SignedAmount::from_sat(110_000), - funding_feerate_sat_per_1000_weight: funding_feerate_sat_per_1000_weight * 3, - ..context - }; - let gross_change = - 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)), - ); - } - fn do_verify_tx_signatures( transaction: Transaction, prev_outputs: Vec, ) -> Result<(), String> { diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index cc422d650a7..6138c2616cb 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -13,7 +13,6 @@ use crate::chain::chaininterface::{TransactionType, 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, WalletSync}; use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType}; use crate::ln::chan_utils; use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY; @@ -26,12 +25,16 @@ use crate::ln::types::ChannelId; use crate::routing::router::{PaymentParameters, RouteParameters}; use crate::util::errors::APIError; use crate::util::ser::Writeable; +use crate::util::wallet_utils::{WalletSourceSync, WalletSync}; use crate::sync::Arc; +use bitcoin::hashes::Hash; use bitcoin::secp256k1::ecdsa::Signature; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use bitcoin::{ + Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash, +}; #[test] fn test_splicing_not_supported_api_error() { @@ -218,6 +221,21 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( 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: FundingContribution, new_funding_script: ScriptBuf, +) { + complete_interactive_funding_negotiation_for_both( + initiator, + acceptor, + channel_id, + initiator_contribution, + None, + new_funding_script, + ); +} + +pub fn complete_interactive_funding_negotiation_for_both<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + initiator_contribution: FundingContribution, + acceptor_contribution: Option, new_funding_script: ScriptBuf, ) { let node_id_initiator = initiator.node.get_our_node_id(); let node_id_acceptor = acceptor.node.get_our_node_id(); @@ -243,8 +261,22 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( .chain(core::iter::once(new_funding_script)) .collect::>(); + let (mut expected_acceptor_inputs, mut expected_acceptor_scripts) = + if let Some(acceptor_contribution) = acceptor_contribution { + let (acceptor_inputs, acceptor_outputs) = acceptor_contribution.into_tx_parts(); + let expected_acceptor_inputs = + acceptor_inputs.iter().map(|input| input.utxo.outpoint).collect::>(); + let expected_acceptor_scripts = + acceptor_outputs.into_iter().map(|output| output.script_pubkey).collect::>(); + (expected_acceptor_inputs, expected_acceptor_scripts) + } else { + (Vec::new(), Vec::new()) + }; + let mut acceptor_sent_tx_complete = false; + let mut initiator_sent_tx_complete; loop { + // Initiator's turn: send TxAddInput, TxAddOutput, or TxComplete if !expected_initiator_inputs.is_empty() { let tx_add_input = get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); @@ -261,6 +293,7 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( expected_initiator_inputs.iter().position(|input| *input == input_prevout).unwrap(), ); acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); + initiator_sent_tx_complete = false; } else if !expected_initiator_scripts.is_empty() { let tx_add_output = get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor); @@ -271,6 +304,7 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( .unwrap(), ); acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output); + initiator_sent_tx_complete = false; } else { let msg_events = initiator.node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), 1, "{msg_events:?}"); @@ -279,24 +313,69 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( } else { panic!(); } + initiator_sent_tx_complete = true; if acceptor_sent_tx_complete { break; } } - let mut msg_events = acceptor.node.get_and_clear_pending_msg_events(); + // Acceptor's turn: send TxAddInput, TxAddOutput, or TxComplete + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendTxComplete { ref msg, .. } = msg_events.remove(0) { - initiator.node.handle_tx_complete(node_id_acceptor, msg); - } else { - panic!(); + match &msg_events[0] { + MessageSendEvent::SendTxAddInput { msg, .. } => { + let input_prevout = BitcoinOutPoint { + txid: msg + .prevtx + .as_ref() + .map(|prevtx| prevtx.compute_txid()) + .or(msg.shared_input_txid) + .unwrap(), + vout: msg.prevtx_out, + }; + expected_acceptor_inputs.remove( + expected_acceptor_inputs + .iter() + .position(|input| *input == input_prevout) + .unwrap(), + ); + initiator.node.handle_tx_add_input(node_id_acceptor, msg); + acceptor_sent_tx_complete = false; + }, + MessageSendEvent::SendTxAddOutput { msg, .. } => { + expected_acceptor_scripts.remove( + expected_acceptor_scripts + .iter() + .position(|script| *script == msg.script) + .unwrap(), + ); + initiator.node.handle_tx_add_output(node_id_acceptor, msg); + acceptor_sent_tx_complete = false; + }, + MessageSendEvent::SendTxComplete { msg, .. } => { + initiator.node.handle_tx_complete(node_id_acceptor, msg); + acceptor_sent_tx_complete = true; + if initiator_sent_tx_complete { + break; + } + }, + _ => panic!("Unexpected message event: {:?}", msg_events[0]), } - acceptor_sent_tx_complete = true; } + + assert!(expected_acceptor_inputs.is_empty(), "Not all acceptor inputs were sent"); + assert!(expected_acceptor_scripts.is_empty(), "Not all acceptor outputs were sent"); } pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, is_0conf: bool, +) -> (Transaction, Option<(msgs::SpliceLocked, PublicKey)>) { + sign_interactive_funding_tx_with_acceptor_contribution(initiator, acceptor, is_0conf, false) +} + +pub fn sign_interactive_funding_tx_with_acceptor_contribution<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, is_0conf: bool, + acceptor_has_contribution: bool, ) -> (Transaction, Option<(msgs::SpliceLocked, PublicKey)>) { let node_id_initiator = initiator.node.get_our_node_id(); let node_id_acceptor = acceptor.node.get_our_node_id(); @@ -330,6 +409,29 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( }; acceptor.node.handle_commitment_signed(node_id_initiator, &initial_commit_sig_for_acceptor); + if acceptor_has_contribution { + // When the acceptor contributed inputs, it needs to sign as well. The counterparty's + // commitment_signed is buffered until the acceptor signs. + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + let event = get_event!(acceptor, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = acceptor.wallet_source.sign_tx(unsigned_transaction).unwrap(); + acceptor + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + panic!(); + } + } + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), 2, "{msg_events:?}"); if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { @@ -548,7 +650,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }]; - let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); // Attempt a splice negotiation that only goes up to receiving `splice_init`. Reconnecting // should implicitly abort the negotiation and reset the splice state such that we're able to @@ -586,14 +689,15 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { nodes[1].node.peer_disconnected(node_id_0); } - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution); let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); reconnect_args.send_channel_ready = (true, true); reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); // Attempt a splice negotiation that ends mid-construction of the funding transaction. // Reconnecting should implicitly abort the negotiation and reset the splice state such that @@ -636,14 +740,15 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { nodes[1].node.peer_disconnected(node_id_0); } - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution); let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); reconnect_args.send_channel_ready = (true, true); reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); // Attempt a splice negotiation that ends before the initial `commitment_signed` messages are // exchanged. The node missing the other's `commitment_signed` upon reconnecting should @@ -717,7 +822,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let tx_abort = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1); nodes[1].node.handle_tx_abort(node_id_0, &tx_abort); - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution); // Attempt a splice negotiation that completes, (i.e. `tx_signatures` are exchanged). Reconnecting // should not abort the negotiation or reset the splice state. @@ -778,7 +883,8 @@ fn test_config_reject_inbound_splices() { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }]; - let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()); let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); nodes[1].node.handle_stfu(node_id_0, &stfu); @@ -799,7 +905,7 @@ fn test_config_reject_inbound_splices() { nodes[0].node.peer_disconnected(node_id_1); nodes[1].node.peer_disconnected(node_id_0); - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution); let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); reconnect_args.send_channel_ready = (true, true); @@ -1607,18 +1713,18 @@ fn test_propose_splice_while_disconnected() { #[cfg(test)] fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { // Test that both nodes are able to propose a splice while the counterparty is disconnected, and - // whoever doesn't go first due to the quiescence tie-breaker, will retry their splice after the - // first one becomes locked. + // whoever doesn't go first due to the quiescence tie-breaker, will have their contribution + // merged into the counterparty-initiated splice. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let (persister_0a, persister_0b, persister_1a, persister_1b); - let (chain_monitor_0a, chain_monitor_0b, chain_monitor_1a, chain_monitor_1b); + let (persister_0a, persister_1a); + let (chain_monitor_0a, chain_monitor_1a); let mut config = test_default_channel_config(); if use_0conf { config.channel_handshake_limits.trust_own_funding_0conf = true; } let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); - let (node_0a, node_0b, node_1a, node_1b); + let (node_0a, node_1a); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_id_0 = nodes[0].node.get_our_node_id(); @@ -1657,15 +1763,8 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { value: Amount::from_sat(splice_out_sat), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }]; - let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); - let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); - let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); - let node_0_funding_contribution = - funding_template.splice_out_sync(node_0_outputs, &wallet).unwrap(); - nodes[0] - .node - .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) - .unwrap(); + let mut node_0_funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, node_0_outputs); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); @@ -1673,14 +1772,8 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { value: Amount::from_sat(splice_out_sat), script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), }]; - let funding_template = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate).unwrap(); - let wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); - let node_1_funding_contribution = - funding_template.splice_out_sync(node_1_outputs, &wallet).unwrap(); - nodes[1] - .node - .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) - .unwrap(); + let mut node_1_funding_contribution = + initiate_splice_out(&nodes[1], &nodes[0], channel_id, node_1_outputs); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -1703,6 +1796,21 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { chain_monitor_1a, node_1a ); + + // quiescent_action is not persisted, so re-propose the splice after reload. + let node_0_outputs = vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + node_0_funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, node_0_outputs); + + let node_1_outputs = vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]; + node_1_funding_contribution = + initiate_splice_out(&nodes[1], &nodes[0], channel_id, node_1_outputs); } // Reconnect the nodes. Both nodes should attempt quiescence as the initiator, but only one will @@ -1725,23 +1833,28 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) .unwrap(); - // Negotiate the first splice to completion. + // Negotiate the splice to completion. Node 1's quiescent action should be consumed by + // splice_init, so both contributions are merged into a single splice. nodes[1].node.handle_splice_init(node_id_0, &splice_init); let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); let new_funding_script = chan_utils::make_funding_redeemscript( &splice_init.funding_pubkey, &splice_ack.funding_pubkey, ) .to_p2wsh(); - complete_interactive_funding_negotiation( + complete_interactive_funding_negotiation_for_both( &nodes[0], &nodes[1], channel_id, node_0_funding_contribution, + Some(node_1_funding_contribution), new_funding_script, ); - let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], use_0conf); + let (splice_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution( + &nodes[0], &nodes[1], use_0conf, true, + ); expect_splice_pending_event(&nodes[0], &node_id_1); expect_splice_pending_event(&nodes[1], &node_id_0); @@ -1755,7 +1868,7 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); - // Mine enough blocks for the first splice to become locked. + // Mine enough blocks for the splice to become locked. connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); @@ -1763,10 +1876,9 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { }; nodes[1].node.handle_splice_locked(node_id_0, &splice_locked); - // We should see the node which lost the tie-breaker attempt their splice now by first - // negotiating quiescence, but their `stfu` won't be sent until after another reconnection. + // Node 1's quiescent action was consumed, so it should NOT send stfu. let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), if use_0conf { 2 } else { 3 }, "{msg_events:?}"); + assert_eq!(msg_events.len(), if use_0conf { 1 } else { 2 }, "{msg_events:?}"); if let MessageSendEvent::SendSpliceLocked { ref msg, .. } = &msg_events[0] { nodes[0].node.handle_splice_locked(node_id_1, msg); if use_0conf { @@ -1787,10 +1899,6 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { panic!("Unexpected event {:?}", &msg_events[1]); } } - assert!(matches!( - &msg_events[if use_0conf { 1 } else { 2 }], - MessageSendEvent::SendStfu { .. } - )); let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), if use_0conf { 0 } else { 2 }, "{msg_events:?}"); @@ -1823,78 +1931,6 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { .chain_source .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); - // Reconnect the nodes. This should trigger the node which lost the tie-breaker to resend `stfu` - // for their splice attempt. - if reload { - let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); - reload_node!( - nodes[0], - nodes[0].node.encode(), - &[&encoded_monitor_0], - persister_0b, - chain_monitor_0b, - node_0b - ); - let encoded_monitor_1 = get_monitor!(nodes[1], channel_id).encode(); - reload_node!( - nodes[1], - nodes[1].node.encode(), - &[&encoded_monitor_1], - persister_1b, - chain_monitor_1b, - node_1b - ); - } else { - nodes[0].node.peer_disconnected(node_id_1); - nodes[1].node.peer_disconnected(node_id_0); - } - let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); - if !use_0conf { - reconnect_args.send_announcement_sigs = (true, true); - } - reconnect_args.send_stfu = (true, false); - reconnect_nodes(reconnect_args); - - // Drive the second splice to completion. - let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendStfu { ref msg, .. } = msg_events[0] { - nodes[1].node.handle_stfu(node_id_0, msg); - } else { - panic!("Unexpected event {:?}", &msg_events[0]); - } - - let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); - nodes[0].node.handle_splice_init(node_id_1, &splice_init); - let splice_ack = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceAck, node_id_1); - nodes[1].node.handle_splice_ack(node_id_0, &splice_ack); - let new_funding_script = chan_utils::make_funding_redeemscript( - &splice_init.funding_pubkey, - &splice_ack.funding_pubkey, - ) - .to_p2wsh(); - complete_interactive_funding_negotiation( - &nodes[1], - &nodes[0], - channel_id, - node_1_funding_contribution, - new_funding_script, - ); - let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[1], &nodes[0], use_0conf); - expect_splice_pending_event(&nodes[0], &node_id_1); - expect_splice_pending_event(&nodes[1], &node_id_0); - - if use_0conf { - let (splice_locked, for_node_id) = splice_locked.unwrap(); - assert_eq!(for_node_id, node_id_0); - lock_splice(&nodes[1], &nodes[0], &splice_locked, true); - } else { - assert!(splice_locked.is_none()); - mine_transaction(&nodes[0], &splice_tx); - mine_transaction(&nodes[1], &splice_tx); - lock_splice_after_blocks(&nodes[1], &nodes[0], ANTI_REORG_DELAY - 1); - } - // Sanity check that we can still make a test payment. send_payment(&nodes[0], &[&nodes[1]], 1_000_000); } @@ -1970,14 +2006,7 @@ fn fail_splice_on_interactive_tx_error() { get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); initiator.node.handle_tx_add_input(node_id_acceptor, &tx_add_input); - let event = get_event!(initiator, Event::SpliceFailed); - match event { - Event::SpliceFailed { contributed_inputs, .. } => { - assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], funding_contribution.into_tx_parts().0[0].outpoint()); - }, - _ => panic!("Expected Event::SpliceFailed"), - } + expect_splice_failed_events(initiator, &channel_id, funding_contribution); let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort); @@ -2024,14 +2053,7 @@ fn fail_splice_on_tx_abort() { let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); - let event = get_event!(initiator, Event::SpliceFailed); - match event { - Event::SpliceFailed { contributed_inputs, .. } => { - assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], funding_contribution.into_tx_parts().0[0].outpoint()); - }, - _ => panic!("Expected Event::SpliceFailed"), - } + expect_splice_failed_events(initiator, &channel_id, funding_contribution); let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort); @@ -2073,7 +2095,7 @@ fn fail_splice_on_channel_close() { &nodes[0], &[ExpectedCloseEvent { channel_id: Some(channel_id), - discard_funding: false, + discard_funding: true, splice_failed: true, channel_funding_txo: None, user_channel_id: Some(42), @@ -2119,7 +2141,7 @@ fn fail_quiescent_action_on_channel_close() { &nodes[0], &[ExpectedCloseEvent { channel_id: Some(channel_id), - discard_funding: false, + discard_funding: true, splice_failed: true, channel_funding_txo: None, user_channel_id: Some(42), @@ -2512,3 +2534,1309 @@ fn test_splice_buffer_invalid_commitment_signed_closes_channel() { ); check_added_monitors(&nodes[0], 1); } + +#[test] +fn test_funding_contributed_counterparty_not_found() { + // Tests that calling funding_contributed with an unknown counterparty_node_id returns + // ChannelUnavailable and emits a DiscardFunding event. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + 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, 50_000_000); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap(); + + // Use a fake/unknown public key as counterparty + let fake_node_id = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap()); + + assert_eq!( + nodes[0].node.funding_contributed( + &channel_id, + &fake_node_id, + funding_contribution.clone(), + None + ), + Err(APIError::ChannelUnavailable { + err: format!( + "Can't find a peer matching the passed counterparty node_id {}", + fake_node_id + ), + }) + ); + + expect_discard_funding_event(&nodes[0], &channel_id, funding_contribution); +} + +#[test] +fn test_funding_contributed_channel_not_found() { + // Tests that calling funding_contributed with an unknown channel_id returns + // ChannelUnavailable and emits a DiscardFunding event. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + 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, 50_000_000); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap(); + + // Use a random/unknown channel_id + let fake_channel_id = ChannelId::from_bytes([42; 32]); + + assert_eq!( + nodes[0].node.funding_contributed( + &fake_channel_id, + &node_id_1, + funding_contribution.clone(), + None + ), + Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} not found for the passed counterparty node_id {}", + fake_channel_id, node_id_1 + ), + }) + ); + + expect_discard_funding_event(&nodes[0], &fake_channel_id, funding_contribution); +} + +#[test] +fn test_funding_contributed_splice_already_pending() { + // Tests that calling funding_contributed when there's already a pending splice + // contribution returns Err(APIMisuseError) and emits a DiscardFunding event containing only the + // inputs/outputs that are NOT already in the existing contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + 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); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 2, splice_in_amount * 2); + + // Use splice_in_and_out with an output so we can test output filtering + let first_splice_out = TxOut { + value: Amount::from_sat(5_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())), + }; + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let first_contribution = funding_template + .splice_in_and_out_sync(splice_in_amount, vec![first_splice_out.clone()], &wallet) + .unwrap(); + + // Initiate a second splice with a DIFFERENT output to test that different outputs + // are included in DiscardFunding (not filtered out) + let second_splice_out = TxOut { + value: Amount::from_sat(6_000), // Different amount + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())), + }; + + // Clear UTXOs and add a LARGER one for the second contribution to ensure + // the change output will be different from the first contribution's change + // + // FIXME: Should we actually not consider the change value given DiscardFunding is meant to + // reclaim the change script pubkey? But that means for other cases we'd need to track which + // output is for change later in the pipeline. + nodes[0].wallet_source.clear_utxos(); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 3); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let second_contribution = funding_template + .splice_in_and_out_sync(splice_in_amount, vec![second_splice_out.clone()], &wallet) + .unwrap(); + + // First funding_contributed - this sets up the quiescent action + nodes[0].node.funding_contributed(&channel_id, &node_id_1, first_contribution, None).unwrap(); + + // Drain the pending stfu message + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Second funding_contributed with a different contribution - this should trigger + // DiscardFunding because there's already a pending quiescent action (splice contribution). + // Only inputs/outputs NOT in the existing contribution should be discarded. + let (expected_inputs, expected_outputs) = + second_contribution.clone().into_contributed_inputs_and_outputs(); + + // Returns Err(APIMisuseError) and emits DiscardFunding for the non-duplicate parts of the second contribution + assert_eq!( + nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None), + Err(APIError::APIMisuseError { + err: format!("Channel {} already has a pending funding contribution", channel_id), + }) + ); + + // The second contribution has different outputs (second_splice_out differs from first_splice_out), + // so those outputs should NOT be filtered out - they should appear in DiscardFunding. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::DiscardFunding { channel_id: event_channel_id, funding_info } => { + assert_eq!(event_channel_id, &channel_id); + if let FundingInfo::Contribution { inputs, outputs } = funding_info { + // The input is different, so it should be in the discard event + assert_eq!(*inputs, expected_inputs); + // The splice-out output is different (6000 vs 5000), so it should be in discard event + assert!(expected_outputs.contains(&second_splice_out)); + assert!(!expected_outputs.contains(&first_splice_out)); + // The different outputs should NOT be filtered out + assert_eq!(*outputs, expected_outputs); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Expected DiscardFunding event"), + } +} + +#[test] +fn test_funding_contributed_duplicate_contribution_no_event() { + // Tests that calling funding_contributed with the exact same contribution twice + // returns Err(APIMisuseError) and emits no events on the second call (DoNothing path). + // This tests the case where all inputs/outputs in the second contribution + // are already present in the existing contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + 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); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap(); + + // First funding_contributed - this sets up the quiescent action + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None).unwrap(); + + // Drain the pending stfu message + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Second funding_contributed with the SAME contribution (same inputs/outputs) + // This should trigger the DoNothing path because all inputs/outputs are duplicates. + // Returns Err(APIMisuseError) and emits NO events. + assert_eq!( + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution, None), + Err(APIError::APIMisuseError { + err: format!("Duplicate funding contribution for channel {}", channel_id), + }) + ); + + // Verify no events were emitted - the duplicate contribution is silently ignored + let events = nodes[0].node.get_and_clear_pending_events(); + assert!(events.is_empty(), "Expected no events for duplicate contribution, got {:?}", events); +} + +#[test] +fn test_funding_contributed_channel_shutdown() { + // Tests that calling funding_contributed after initiating channel shutdown returns Err(APIMisuseError) + // and emits both SpliceFailed and DiscardFunding events. The channel is no longer usable + // after shutdown is initiated, so quiescence cannot be proposed. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + 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); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap(); + + // Initiate channel shutdown - this makes is_usable() return false + nodes[0].node.close_channel(&channel_id, &node_id_1).unwrap(); + + // Drain the pending shutdown message + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendShutdown, node_id_1); + + // Now call funding_contributed - this should trigger FailSplice because + // propose_quiescence() will fail when is_usable() returns false. + // Returns Err(APIMisuseError) and emits both SpliceFailed and DiscardFunding. + assert_eq!( + nodes[0].node.funding_contributed( + &channel_id, + &node_id_1, + funding_contribution.clone(), + None + ), + Err(APIError::APIMisuseError { + err: format!("Channel {} cannot accept funding contribution", channel_id), + }) + ); + + expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution); +} + +#[test] +fn test_funding_contributed_unfunded_channel() { + // Tests that calling funding_contributed on an unfunded channel returns APIMisuseError + // and emits a DiscardFunding event. The channel exists but is not yet funded. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + // Create a funded channel for the splice operation + let (_, _, funded_channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + // Create an unfunded channel (after open/accept but before funding tx) + let unfunded_channel_id = exchange_open_accept_chan(&nodes[0], &nodes[1], 50_000, 0); + + // Drain the FundingGenerationReady event for the unfunded channel + let _ = get_event!(nodes[0], Event::FundingGenerationReady); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = + nodes[0].node.splice_channel(&funded_channel_id, &node_id_1, feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template.splice_in_sync(splice_in_amount, &wallet).unwrap(); + + // Call funding_contributed with the unfunded channel's ID instead of the funded one. + // Returns APIMisuseError because the channel is not funded. + assert_eq!( + nodes[0].node.funding_contributed( + &unfunded_channel_id, + &node_id_1, + funding_contribution.clone(), + None + ), + Err(APIError::APIMisuseError { + err: format!( + "Channel with id {} not expecting funding contribution", + unfunded_channel_id + ), + }) + ); + + expect_discard_funding_event(&nodes[0], &unfunded_channel_id, funding_contribution); +} + +// Helper to re-enter quiescence between two nodes where node_a is the initiator. +// Returns after both sides are quiescent (no splice_init is generated since we use DoNothing). +fn reenter_quiescence<'a, 'b, 'c>( + node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_id: &ChannelId, +) { + let node_id_a = node_a.node.get_our_node_id(); + let node_id_b = node_b.node.get_our_node_id(); + + node_a.node.maybe_propose_quiescence(&node_id_b, channel_id).unwrap(); + let stfu_a = get_event_msg!(node_a, MessageSendEvent::SendStfu, node_id_b); + node_b.node.handle_stfu(node_id_a, &stfu_a); + let stfu_b = get_event_msg!(node_b, MessageSendEvent::SendStfu, node_id_a); + node_a.node.handle_stfu(node_id_b, &stfu_b); +} + +#[test] +fn test_splice_rbf_acceptor_basic() { + // Test the full end-to-end flow for RBF of a pending splice transaction. + // Complete a splice-in, then use rbf_channel API to initiate an RBF attempt + // with a higher feerate, going through the full tx_init_rbf → tx_ack_rbf → + // interactive TX → signing → mining → splice_locked flow. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 1: Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + // Save the pre-splice funding outpoint before splice_channel modifies the monitor. + let original_funding_outpoint = nodes[0] + .chain_monitor + .chain_monitor + .get_monitor(channel_id) + .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .unwrap(); + + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Step 2: Provide more UTXO reserves for the RBF attempt. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 3: Use rbf_channel API to initiate the RBF. + // Original feerate was FEERATE_FLOOR_SATS_PER_KW (253). 253 * 25 / 24 = 263.54, so 264 works. + let rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24; // ceil(253*25/24) = 264 + let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); + let funding_template = nodes[0].node.rbf_channel(&channel_id, &node_id_1, rbf_feerate).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template.splice_in_sync(added_value, &wallet).unwrap(); + + // Step 4: funding_contributed stores QuiescentAction::Splice and proposes quiescence. + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, funding_contribution.clone(), None) + .unwrap(); + + // Step 5: STFU exchange. + let stfu_a = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_a); + let stfu_b = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_b); + + // Step 6: Node 0 sends tx_init_rbf (not splice_init, since pending_splice exists). + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.channel_id, channel_id); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, rbf_feerate_sat_per_kwu as u32); + + // Step 7: Node 1 handles tx_init_rbf → responds with tx_ack_rbf. + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); + assert_eq!(tx_ack_rbf.channel_id, channel_id); + + // Step 8: Node 0 handles tx_ack_rbf → starts interactive TX construction. + nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf); + + // Step 9: Complete interactive funding negotiation. + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution, + new_funding_script.clone(), + ); + + // Step 10: Sign and broadcast. + let (rbf_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 11: Mine and lock. + mine_transaction(&nodes[0], &rbf_tx); + mine_transaction(&nodes[1], &rbf_tx); + + // Lock the RBF splice. We can't use lock_splice_after_blocks directly because the splice + // promotion generates DiscardFunding events for the old (replaced) splice candidate. + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + + let splice_locked_b = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked_b); + + let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + let splice_locked_a = + if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendSpliceLocked, got {:?}", msg_events[0]); + }; + let announcement_sigs_b = + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendAnnouncementSignatures"); + }; + nodes[0].node.handle_splice_locked(node_id_1, &splice_locked_a); + nodes[0].node.handle_announcement_signatures(node_id_1, &announcement_sigs_b); + + // Expect ChannelReady + DiscardFunding for the old splice candidate on both nodes. + let events_a = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events_a.len(), 2, "{events_a:?}"); + assert!(matches!(events_a[0], Event::ChannelReady { .. })); + assert!(matches!(events_a[1], Event::DiscardFunding { .. })); + check_added_monitors(&nodes[0], 1); + + let events_b = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events_b.len(), 2, "{events_b:?}"); + assert!(matches!(events_b[0], Event::ChannelReady { .. })); + assert!(matches!(events_b[1], Event::DiscardFunding { .. })); + check_added_monitors(&nodes[1], 1); + + // Complete the announcement exchange. + let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + nodes[1].node.handle_announcement_signatures(node_id_0, &msg); + } else { + panic!("Expected SendAnnouncementSignatures"); + } + assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + // Clean up old watched outpoints from the chain source. + // The original channel's funding outpoint and the first (replaced) splice's funding outpoint + // are still being watched but are no longer tracked by the deserialized monitor. + let (orig_outpoint, orig_script) = original_funding_outpoint; + let first_splice_funding_idx = + first_splice_tx.output.iter().position(|o| o.script_pubkey == new_funding_script).unwrap(); + let first_splice_outpoint = + OutPoint { txid: first_splice_tx.compute_txid(), index: first_splice_funding_idx as u16 }; + for node in &nodes { + node.chain_source.remove_watched_txn_and_outputs(orig_outpoint, orig_script.clone()); + node.chain_source + .remove_watched_txn_and_outputs(first_splice_outpoint, new_funding_script.clone()); + } +} + +#[test] +fn test_splice_rbf_insufficient_feerate() { + // Test that rbf_channel rejects a feerate that doesn't satisfy the 25/24 rule, and that the + // acceptor also rejects tx_init_rbf with an insufficient feerate from a misbehaving peer. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Initiator-side: rbf_channel rejects an insufficient feerate. + // Original feerate was 253. Using exactly 253 should fail since 253 * 24 < 253 * 25. + let same_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let err = nodes[0].node.rbf_channel(&channel_id, &node_id_1, same_feerate).unwrap_err(); + assert_eq!( + err, + APIError::APIMisuseError { + err: format!( + "Channel {} RBF feerate {} is less than 25/24 of the previous feerate {}", + channel_id, FEERATE_FLOOR_SATS_PER_KW, FEERATE_FLOOR_SATS_PER_KW, + ), + } + ); + + // Acceptor-side: tx_init_rbf with an insufficient feerate is also rejected. + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: format!( + "Channel {} RBF feerate {} is less than 25/24 of the previous feerate {}", + channel_id, FEERATE_FLOOR_SATS_PER_KW, FEERATE_FLOOR_SATS_PER_KW, + ), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } +} + +#[test] +fn test_splice_rbf_no_pending_splice() { + // Test that tx_init_rbf is rejected when there is no pending splice to RBF. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + 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); + + // Re-enter quiescence without having done a splice. + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(50_000), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: format!("Channel {} has no pending splice to RBF", channel_id), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } +} + +#[test] +fn test_splice_rbf_active_negotiation() { + // Test that tx_init_rbf is rejected when a funding negotiation is already in progress. + // Start a splice but don't complete interactive TX construction, then send tx_init_rbf. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Initiate a splice but only complete the handshake (STFU + splice_init/ack), + // leaving interactive TX construction in progress. + let _funding_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let _new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); + + // Now the acceptor (node 1) has a funding_negotiation in progress (ConstructingTransaction). + // Sending tx_init_rbf should be rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: format!( + "Channel {} already has a funding negotiation in progress", + channel_id, + ), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } + + // Clear the initiator's pending interactive TX messages from the incomplete splice handshake. + nodes[0].node.get_and_clear_pending_msg_events(); +} + +#[test] +fn test_splice_rbf_not_quiescence_initiator() { + // Test that tx_init_rbf is rejected when the sender is not the quiescence initiator. + // Node 1 initiates quiescence, so only node 1 should be allowed to send tx_init_rbf. + // Node 0 sending tx_init_rbf should be rejected. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Re-enter quiescence with node 1 as the initiator (not node 0). + nodes[1].node.maybe_propose_quiescence(&node_id_0, &channel_id).unwrap(); + let stfu_b = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_b); + let stfu_a = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_a); + + // Node 0 sends tx_init_rbf, but node 1 is the quiescence initiator, so node 0 should be + // rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: "Counterparty sent tx_init_rbf but is not the quiescence initiator" + .to_owned(), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } +} + +#[test] +fn test_splice_rbf_after_splice_locked() { + // Test that tx_init_rbf is rejected when the counterparty has already sent splice_locked. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Mine the splice tx on both nodes. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + // Connect enough blocks on node 0 only so it sends splice_locked. + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + + let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + + // Deliver splice_locked to node 1. Since node 1 hasn't confirmed enough blocks, + // it won't send its own splice_locked back, but it will set received_funding_txid. + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked); + + // Node 1 shouldn't have any messages to send (no splice_locked since it hasn't confirmed). + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert!(msg_events.is_empty(), "Expected no messages, got {:?}", msg_events); + + // Re-enter quiescence (node 0 initiates). + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + // Node 0 sends tx_init_rbf, but node 0 already sent splice_locked, so it should be rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: format!( + "Channel {} counterparty already sent splice_locked, cannot RBF", + channel_id, + ), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } +} + +#[test] +fn test_splice_rbf_zeroconf_rejected() { + // Test that tx_init_rbf is rejected when option_zeroconf is negotiated. + // The zero-conf check happens before the pending_splice check, so we don't need to complete + // a splice — just enter quiescence and send tx_init_rbf. + 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_limits.trust_own_funding_0conf = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + + // Enter quiescence (node 0 initiates). + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + // Node 0 sends tx_init_rbf, but the channel has option_zeroconf, so it should be rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(50_000), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: format!( + "Channel {} has option_zeroconf, cannot RBF splice", + channel_id, + ), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } +} + +#[test] +fn test_splice_rbf_both_contribute_tiebreak() { + do_test_splice_rbf_both_contribute_tiebreak(None, None); +} + +#[test] +fn test_splice_rbf_tiebreak_higher_feerate() { + // Node 0 (winner) uses a higher feerate than node 1 (loser). Node 1's change output is + // adjusted (reduced) to accommodate the higher feerate. Negotiation succeeds. + let min_rbf_feerate = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24; + do_test_splice_rbf_both_contribute_tiebreak( + Some(FeeRate::from_sat_per_kwu(min_rbf_feerate * 3)), + Some(FeeRate::from_sat_per_kwu(min_rbf_feerate)), + ); +} + +#[test] +fn test_splice_rbf_tiebreak_lower_feerate() { + // Node 0 (winner) uses a lower feerate than node 1 (loser). Node 1's change output increases + // because the acceptor's fair fee decreases. Negotiation succeeds. + let min_rbf_feerate = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24; + do_test_splice_rbf_both_contribute_tiebreak( + Some(FeeRate::from_sat_per_kwu(min_rbf_feerate)), + Some(FeeRate::from_sat_per_kwu(min_rbf_feerate * 3)), + ); +} + +/// Runs the tie-breaker test with optional per-node feerates. +/// If `node_0_feerate` or `node_1_feerate` is None, both use the same default RBF feerate. +fn do_test_splice_rbf_both_contribute_tiebreak( + node_0_feerate: Option, node_1_feerate: Option, +) { + // Test where both parties call rbf_channel + funding_contributed, both send STFU, one wins + // the quiescence tie-break (node 0, the outbound channel funder). The loser (node 1) becomes + // the acceptor and its stored QuiescentAction::Splice is consumed by the tx_init_rbf handler, + // contributing its inputs/outputs to the RBF transaction. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 1: Complete an initial splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let original_funding_outpoint = nodes[0] + .chain_monitor + .chain_monitor + .get_monitor(channel_id) + .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .unwrap(); + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Step 2: Provide more UTXOs for both nodes' RBF attempts. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 3: Both nodes initiate RBF, possibly at different feerates. + let default_rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24; + let default_rbf_feerate = FeeRate::from_sat_per_kwu(default_rbf_feerate_sat_per_kwu); + let rbf_feerate_0 = node_0_feerate.unwrap_or(default_rbf_feerate); + let rbf_feerate_1 = node_1_feerate.unwrap_or(default_rbf_feerate); + + // Node 0 calls rbf_channel + funding_contributed. + let funding_template_0 = + nodes[0].node.rbf_channel(&channel_id, &node_id_1, rbf_feerate_0).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = + funding_template_0.splice_in_sync(added_value, &wallet_0).unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + // Node 1 calls rbf_channel + funding_contributed. + let funding_template_1 = + nodes[1].node.rbf_channel(&channel_id, &node_id_0, rbf_feerate_1).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = + funding_template_1.splice_in_sync(added_value, &wallet_1).unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Step 4: Both nodes sent STFU (both have awaiting_quiescence set). + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + assert!(stfu_0.initiator); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + assert!(stfu_1.initiator); + + // Step 5: Exchange STFUs. Node 0 is the outbound channel funder and wins the tie-break. + // Node 1 handles node 0's STFU first — it already sent its own STFU (local_stfu_sent is set), + // so this goes through the tie-break path. Node 1 loses (is_outbound = false) and becomes the + // acceptor. Its quiescent_action is preserved for the tx_init_rbf handler. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Node 0 handles node 1's STFU — it already sent its own STFU, so tie-break again. + // Node 0 wins (is_outbound = true), consumes its quiescent_action, and sends tx_init_rbf. + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Step 6: Node 0 sends tx_init_rbf. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.channel_id, channel_id); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, rbf_feerate_0.to_sat_per_kwu() as u32); + + // Step 7: Node 1 handles tx_init_rbf — its quiescent_action is consumed, providing its + // inputs/outputs (adjusted for node 0's feerate). Responds with tx_ack_rbf. + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); + assert_eq!(tx_ack_rbf.channel_id, channel_id); + assert!( + tx_ack_rbf.funding_output_contribution.is_some(), + "Acceptor should contribute to the RBF splice" + ); + + // Step 8: Node 0 handles tx_ack_rbf. + nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf); + + // Step 9: Complete interactive funding negotiation with both parties' inputs/outputs. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution), + new_funding_script.clone(), + ); + + // Step 10: Sign (acceptor has contribution) and broadcast. + let (rbf_tx, splice_locked) = + sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, true); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 11: Mine and lock. + mine_transaction(&nodes[0], &rbf_tx); + mine_transaction(&nodes[1], &rbf_tx); + + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + + let splice_locked_b = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked_b); + + let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + let splice_locked_a = + if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendSpliceLocked, got {:?}", msg_events[0]); + }; + let announcement_sigs_b = + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendAnnouncementSignatures"); + }; + nodes[0].node.handle_splice_locked(node_id_1, &splice_locked_a); + nodes[0].node.handle_announcement_signatures(node_id_1, &announcement_sigs_b); + + // Expect ChannelReady + DiscardFunding for the old splice candidate on both nodes. + let events_a = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events_a.len(), 2, "{events_a:?}"); + assert!(matches!(events_a[0], Event::ChannelReady { .. })); + assert!(matches!(events_a[1], Event::DiscardFunding { .. })); + check_added_monitors(&nodes[0], 1); + + let events_b = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events_b.len(), 2, "{events_b:?}"); + assert!(matches!(events_b[0], Event::ChannelReady { .. })); + assert!(matches!(events_b[1], Event::DiscardFunding { .. })); + check_added_monitors(&nodes[1], 1); + + // Complete the announcement exchange. + let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + nodes[1].node.handle_announcement_signatures(node_id_0, &msg); + } else { + panic!("Expected SendAnnouncementSignatures"); + } + assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + // Clean up old watched outpoints from the chain source. + let (orig_outpoint, orig_script) = original_funding_outpoint; + let first_splice_funding_idx = + first_splice_tx.output.iter().position(|o| o.script_pubkey == new_funding_script).unwrap(); + let first_splice_outpoint = + OutPoint { txid: first_splice_tx.compute_txid(), index: first_splice_funding_idx as u16 }; + for node in &nodes { + node.chain_source.remove_watched_txn_and_outputs(orig_outpoint, orig_script.clone()); + node.chain_source + .remove_watched_txn_and_outputs(first_splice_outpoint, new_funding_script.clone()); + } +} + +#[test] +fn test_splice_rbf_tiebreak_feerate_too_high() { + // Node 0 (winner) uses a feerate high enough that node 1's (loser) contribution cannot + // cover the fees. Node 1 proceeds without its contribution (QuiescentAction is preserved + // for a future splice). The RBF completes with only node 0's inputs/outputs. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + 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 added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete an initial splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let original_funding_outpoint = nodes[0] + .chain_monitor + .chain_monitor + .get_monitor(channel_id) + .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .unwrap(); + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Provide more UTXOs for both nodes' RBF attempts. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Node 0 uses a high feerate (20,000 sat/kwu). Node 1 uses the minimum RBF feerate but + // splices in a large amount (95,000 sats from a 100,000 sat UTXO), leaving very little + // change/fee budget. Node 1's budget (~5,000 sats) can't cover the acceptor's fair fee + // at 20,000 sat/kwu (~5,440 sats without change output), so adjust_for_feerate fails. + let high_feerate = FeeRate::from_sat_per_kwu(20_000); + let min_rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24; + let min_rbf_feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate_sat_per_kwu); + + let node_1_added_value = Amount::from_sat(95_000); + + let funding_template_0 = + nodes[0].node.rbf_channel(&channel_id, &node_id_1, high_feerate).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = + funding_template_0.splice_in_sync(added_value, &wallet_0).unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + let funding_template_1 = + nodes[1].node.rbf_channel(&channel_id, &node_id_0, min_rbf_feerate).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = + funding_template_1.splice_in_sync(node_1_added_value, &wallet_1).unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Both sent STFU. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + // Tie-break: node 0 wins. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Node 0 sends tx_init_rbf at 20,000 sat/kwu. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, high_feerate.to_sat_per_kwu() as u32); + + // Node 1 handles tx_init_rbf — adjust_for_feerate fails because node 1's contribution + // can't cover fees at 20,000 sat/kwu. Node 1 proceeds without its contribution. + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); + assert_eq!(tx_ack_rbf.channel_id, channel_id); + assert!( + tx_ack_rbf.funding_output_contribution.is_none(), + "Acceptor should not contribute when feerate adjustment fails" + ); + + // Node 0 handles tx_ack_rbf. + nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf); + + // Complete interactive funding negotiation with only node 0's contribution. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + None, + new_funding_script.clone(), + ); + + // Sign (acceptor has no contribution) and broadcast. + let (rbf_tx, splice_locked) = + sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, false); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Mine and lock. + mine_transaction(&nodes[0], &rbf_tx); + mine_transaction(&nodes[1], &rbf_tx); + + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + + let splice_locked_b = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked_b); + + // Node 1's QuiescentAction was preserved, so after splice_locked it re-initiates + // quiescence to retry its contribution in a future splice. + let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 3, "{msg_events:?}"); + let splice_locked_a = + if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendSpliceLocked, got {:?}", msg_events[0]); + }; + let announcement_sigs_b = + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendAnnouncementSignatures"); + }; + let stfu_1 = if let MessageSendEvent::SendStfu { msg, .. } = msg_events.remove(0) { + msg + } else { + panic!("Expected SendStfu, got {:?}", msg_events[0]); + }; + assert!(stfu_1.initiator); + + nodes[0].node.handle_splice_locked(node_id_1, &splice_locked_a); + nodes[0].node.handle_announcement_signatures(node_id_1, &announcement_sigs_b); + + // Expect ChannelReady + DiscardFunding for the old splice candidate on both nodes. + let events_a = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events_a.len(), 2, "{events_a:?}"); + assert!(matches!(events_a[0], Event::ChannelReady { .. })); + assert!(matches!(events_a[1], Event::DiscardFunding { .. })); + check_added_monitors(&nodes[0], 1); + + let events_b = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events_b.len(), 2, "{events_b:?}"); + assert!(matches!(events_b[0], Event::ChannelReady { .. })); + assert!(matches!(events_b[1], Event::DiscardFunding { .. })); + check_added_monitors(&nodes[1], 1); + + // Complete the announcement exchange. + let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + nodes[1].node.handle_announcement_signatures(node_id_0, &msg); + } else { + panic!("Expected SendAnnouncementSignatures"); + } + assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + let mut msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + // Clean up old watched outpoints. + let (orig_outpoint, orig_script) = original_funding_outpoint; + let first_splice_funding_idx = + first_splice_tx.output.iter().position(|o| o.script_pubkey == new_funding_script).unwrap(); + let first_splice_outpoint = + OutPoint { txid: first_splice_tx.compute_txid(), index: first_splice_funding_idx as u16 }; + for node in &nodes { + node.chain_source.remove_watched_txn_and_outputs(orig_outpoint, orig_script.clone()); + node.chain_source + .remove_watched_txn_and_outputs(first_splice_outpoint, new_funding_script.clone()); + } + + // === Part 2: Node 1's preserved QuiescentAction leads to a new splice === + // + // After splice_locked, pending_splice is None. So when stfu() consumes the QuiescentAction, + // it sends SpliceInit (not TxInitRbf), starting a brand new splice. + + // Node 0 receives node 1's STFU and responds with its own STFU. + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Node 1 receives STFU → quiescence established → node 1 is the initiator → sends SpliceInit. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); + + // Node 0 handles SpliceInit → sends SpliceAck. + nodes[0].node.handle_splice_init(node_id_1, &splice_init); + let splice_ack = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceAck, node_id_1); + + // Node 1 handles SpliceAck → starts interactive tx construction. + nodes[1].node.handle_splice_ack(node_id_0, &splice_ack); + + // Compute the new funding script from the splice pubkeys. + let new_funding_script_2 = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + // Complete interactive funding negotiation with node 1 as initiator (only node 1 contributes). + complete_interactive_funding_negotiation( + &nodes[1], + &nodes[0], + channel_id, + node_1_funding_contribution, + new_funding_script_2, + ); + + // Sign (no acceptor contribution) and broadcast. + let (new_splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[1], &nodes[0], false); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[1], &node_id_0); + expect_splice_pending_event(&nodes[0], &node_id_1); + + // Mine and lock. + mine_transaction(&nodes[1], &new_splice_tx); + mine_transaction(&nodes[0], &new_splice_tx); + + lock_splice_after_blocks(&nodes[1], &nodes[0], ANTI_REORG_DELAY - 1); +} diff --git a/lightning/src/ln/zero_fee_commitment_tests.rs b/lightning/src/ln/zero_fee_commitment_tests.rs index b7221552603..aa4f012e73c 100644 --- a/lightning/src/ln/zero_fee_commitment_tests.rs +++ b/lightning/src/ln/zero_fee_commitment_tests.rs @@ -368,7 +368,7 @@ fn test_anchor_tx_too_big() { - EMPTY_WITNESS_WEIGHT - P2WSH_TXOUT_WEIGHT; nodes[1].logger.assert_log( - "lightning::events::bump_transaction", + "lightning::util::wallet_utils", format!( "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", FEERATE, max_coin_selection_weight @@ -402,7 +402,7 @@ fn test_anchor_tx_too_big() { assert_eq!(txns[1].input.len(), 2); assert_eq!(txns[1].output.len(), 1); nodes[1].logger.assert_log( - "lightning::events::bump_transaction", + "lightning::util::wallet_utils", format!( "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", FEERATE, max_coin_selection_weight diff --git a/lightning/src/util/anchor_channel_reserves.rs b/lightning/src/util/anchor_channel_reserves.rs index 25a0e7ca0ba..2c09ddd70a6 100644 --- a/lightning/src/util/anchor_channel_reserves.rs +++ b/lightning/src/util/anchor_channel_reserves.rs @@ -24,7 +24,6 @@ use crate::chain::chaininterface::FeeEstimator; use crate::chain::chainmonitor::ChainMonitor; use crate::chain::chainmonitor::Persist; use crate::chain::Filter; -use crate::events::bump_transaction::Utxo; use crate::ln::chan_utils::max_htlcs; use crate::ln::channelmanager::AChannelManager; use crate::prelude::new_hash_set; @@ -32,6 +31,7 @@ use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::EntropySource; use crate::types::features::ChannelTypeFeatures; use crate::util::logger::Logger; +use crate::util::wallet_utils::Utxo; use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::Amount; use bitcoin::FeeRate; diff --git a/lightning/src/util/mod.rs b/lightning/src/util/mod.rs index dcbea904b51..75434fdabab 100644 --- a/lightning/src/util/mod.rs +++ b/lightning/src/util/mod.rs @@ -51,6 +51,7 @@ pub(crate) mod macro_logger; // These have to come after macro_logger to build pub mod config; pub mod logger; +pub mod wallet_utils; #[cfg(any(test, feature = "_test_utils"))] pub mod test_utils; diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 02b63a61eae..22be4367c7a 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -21,8 +21,6 @@ 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::{ConfirmedUtxo, Utxo}; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::channel_state::ChannelDetails; @@ -62,6 +60,7 @@ use crate::util::persist::{KVStore, KVStoreSync, MonitorName}; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use crate::util::test_channel_signer::{EnforcementState, TestChannelSigner}; use crate::util::wakers::Notifier; +use crate::util::wallet_utils::{ConfirmedUtxo, Utxo, WalletSourceSync}; use bitcoin::amount::Amount; use bitcoin::block::Block; diff --git a/lightning/src/util/wallet_utils.rs b/lightning/src/util/wallet_utils.rs new file mode 100644 index 00000000000..b82437c03e8 --- /dev/null +++ b/lightning/src/util/wallet_utils.rs @@ -0,0 +1,978 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Utilities for wallet integration with LDK. + +use core::future::Future; +use core::ops::Deref; +use core::pin::pin; +use core::task; + +use crate::chain::chaininterface::fee_for_weight; +use crate::chain::ClaimId; +use crate::io_extras::sink; +use crate::ln::chan_utils::{ + BASE_INPUT_WEIGHT, BASE_TX_SIZE, EMPTY_SCRIPT_SIG_WEIGHT, P2WSH_TXOUT_WEIGHT, + SEGWIT_MARKER_FLAG_WEIGHT, +}; +use crate::prelude::*; +use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +use crate::sync::Mutex; +use crate::util::async_poll::{dummy_waker, MaybeSend, MaybeSync}; +use crate::util::hash_tables::{new_hash_map, HashMap}; +use crate::util::logger::Logger; + +use bitcoin::amount::Amount; +use bitcoin::consensus::Encodable; +use bitcoin::constants::WITNESS_SCALE_FACTOR; +use bitcoin::key::TweakedPublicKey; +use bitcoin::{ + OutPoint, Psbt, PubkeyHash, Script, ScriptBuf, Sequence, Transaction, TxOut, WPubkeyHash, + Weight, +}; + +/// An input that must be included in a transaction when performing coin selection through +/// [`CoinSelectionSource::select_confirmed_utxos`]. It is guaranteed to be a SegWit input, so it +/// must have an empty [`TxIn::script_sig`] when spent. +/// +/// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig +#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub struct Input { + /// The unique identifier of the input. + pub outpoint: OutPoint, + /// The UTXO being spent by the input. + pub previous_utxo: TxOut, + /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and + /// [`TxIn::witness`], each with their lengths included, required to satisfy the output's + /// script. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + pub satisfaction_weight: u64, +} + +/// An unspent transaction output that is available to spend resulting from a successful +/// [`CoinSelection`] attempt. +#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub struct Utxo { + /// The unique identifier of the output. + pub outpoint: OutPoint, + /// The output to spend. + pub output: TxOut, + /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and [`TxIn::witness`], each + /// 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`]. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + pub satisfaction_weight: u64, + /// The sequence number to use in the [`TxIn`] when spending the UTXO. + /// + /// [`TxIn`]: bitcoin::TxIn + 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 { + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a legacy P2PKH output. + pub fn new_p2pkh(outpoint: OutPoint, value: Amount, pubkey_hash: &PubkeyHash) -> Self { + let script_sig_size = 1 /* script_sig length */ + + 1 /* OP_PUSH73 */ + + 73 /* sig including sighash flag */ + + 1 /* OP_PUSH33 */ + + 33 /* pubkey */; + Self { + 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, + } + } + + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a P2WPKH nested in P2SH output. + pub fn new_nested_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { + let script_sig_size = 1 /* script_sig length */ + + 1 /* OP_0 */ + + 1 /* OP_PUSH20 */ + + 20 /* pubkey_hash */; + Self { + outpoint, + output: TxOut { + value, + script_pubkey: ScriptBuf::new_p2sh( + &ScriptBuf::new_p2wpkh(pubkey_hash).script_hash(), + ), + }, + satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + } + } + + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a SegWit v0 P2WPKH output. + pub fn new_v0_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { + Self { + 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, + } + } + + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a keypath spend of a SegWit v1 P2TR output. + pub fn new_v1_p2tr( + outpoint: OutPoint, value: Amount, tweaked_public_key: TweakedPublicKey, + ) -> Self { + Self { + outpoint, + output: TxOut { value, script_pubkey: ScriptBuf::new_p2tr_tweaked(tweaked_public_key) }, + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2TR_KEY_PATH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + } + } +} + +/// An unspent transaction output with at least one confirmation. +/// +/// Can be used as an input to contribute to a channel's funding transaction either when using the +/// v2 channel establishment protocol or when splicing. +#[derive(Debug, Clone)] +pub struct ConfirmedUtxo { + /// The unspent [`TxOut`] found in [`prevtx`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`prevtx`]: Self::prevtx + pub(crate) utxo: Utxo, + + /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`utxo`]: Self::utxo + pub(crate) prevtx: Transaction, +} + +impl_writeable_tlv_based!(ConfirmedUtxo, { + (1, utxo, required), + (3, _sequence, (legacy, Sequence, + |read_val: Option<&Sequence>| { + if let Some(sequence) = read_val { + // Utxo contains sequence now, so update it if the value read here differs since + // this indicates Utxo::sequence was read with default_value + let utxo: &mut Utxo = utxo.0.as_mut().expect("utxo is required"); + if utxo.sequence != *sequence { + utxo.sequence = *sequence; + } + } + Ok(()) + }, + |utxo: &ConfirmedUtxo| Some(utxo.utxo.sequence))), + (5, prevtx, required), +}); + +impl ConfirmedUtxo { + fn new bool>( + prevtx: Transaction, vout: u32, witness_weight: Weight, script_filter: F, + ) -> Result { + Ok(ConfirmedUtxo { + utxo: Utxo { + outpoint: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout }, + output: prevtx + .output + .get(vout as usize) + .filter(|output| script_filter(&output.script_pubkey)) + .ok_or(())? + .clone(), + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + }, + prevtx, + }) + } + + /// Creates an input spending a P2WPKH output from the given `prevtx` at index `vout`. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2wpkh(prevtx: Transaction, vout: u32) -> Result { + let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT) + - if cfg!(feature = "grind_signatures") { + // Guarantees a low R signature + Weight::from_wu(1) + } else { + Weight::ZERO + }; + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2wpkh) + } + + /// Creates an input spending a P2WSH output from the given `prevtx` at index `vout`. + /// + /// Requires passing the weight of witness needed to satisfy the output's script. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2wsh(prevtx: Transaction, vout: u32, witness_weight: Weight) -> Result { + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2wsh) + } + + /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. + /// + /// This is meant for inputs spending a taproot output using the key path. See + /// [`new_p2tr_script_spend`] for when spending using a script path. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`new_p2tr_script_spend`]: Self::new_p2tr_script_spend + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2tr_key_spend(prevtx: Transaction, vout: u32) -> Result { + let witness_weight = Weight::from_wu(P2TR_KEY_PATH_WITNESS_WEIGHT); + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2tr) + } + + /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. + /// + /// Requires passing the weight of witness needed to satisfy a script path of the taproot + /// output. See [`new_p2tr_key_spend`] for when spending using the key path. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`new_p2tr_key_spend`]: Self::new_p2tr_key_spend + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2tr_script_spend( + prevtx: Transaction, vout: u32, witness_weight: Weight, + ) -> Result { + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2tr) + } + + #[cfg(test)] + pub(crate) fn new_p2pkh(prevtx: Transaction, vout: u32) -> Result { + ConfirmedUtxo::new(prevtx, vout, Weight::ZERO, Script::is_p2pkh) + } + + /// The outpoint of the UTXO being spent. + pub fn outpoint(&self) -> bitcoin::OutPoint { + 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.utxo.sequence + } + + /// Sets the sequence number to use in the [`TxIn`]. + /// + /// [`TxIn`]: bitcoin::TxIn + pub fn set_sequence(&mut self, sequence: Sequence) { + self.utxo.sequence = sequence; + } + + /// Converts the [`ConfirmedUtxo`] into a [`Utxo`]. + pub fn into_utxo(self) -> Utxo { + self.utxo + } + + /// Converts the [`ConfirmedUtxo`] into a [`TxOut`]. + pub fn into_output(self) -> TxOut { + self.utxo.output + } +} + +/// 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, + /// 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 + /// not met. This implies no other party should be able to spend it except us. + pub change_output: Option, +} + +impl CoinSelection { + pub(crate) fn satisfaction_weight(&self) -> u64 { + self.confirmed_utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.satisfaction_weight).sum() + } + + pub(crate) fn input_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`], +/// which can provide a default implementation of this trait when used with [`Wallet`]. +/// +/// For a synchronous version of this trait, see [`CoinSelectionSourceSync`]. +/// +/// This is not exported to bindings users as async is only supported in Rust. +// Note that updates to documentation on this trait should be copied to the synchronous version. +pub trait CoinSelectionSource { + /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are + /// available to spend. Implementations are free to pick their coin selection algorithm of + /// choice, as long as the following requirements are met: + /// + /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction + /// throughout coin selection, but must not be returned as part of the result. + /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction + /// throughout coin selection. In some cases, like when funding an anchor transaction, this + /// set is empty. Implementations should ensure they handle this correctly on their end, + /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be + /// provided, in which case a zero-value empty OP_RETURN output can be used instead. + /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the + /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. + /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this + /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC + /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for + /// anchor transactions, we will try your coin selection again with the same input-output + /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions + /// cannot be downsized. + /// + /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of + /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require + /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and + /// delaying block inclusion. + /// + /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they + /// can be re-used within new fee-bumped iterations of the original claiming transaction, + /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a + /// transaction associated with it, and all of the available UTXOs have already been assigned to + /// other claims, implementations must be willing to double spend their UTXOs. The choice of + /// which UTXOs to double spend is left to the implementation, but it must strive to keep the + /// set of other claims being double spent to a minimum. + /// + /// If `claim_id` is not set, then the selection should be treated as if it were for a unique + /// claim and must NOT be double-spent rather than being kept to a minimum. + /// + /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future> + MaybeSend + 'a; + /// Signs and provides the full witness for all inputs within the transaction known to the + /// trait (i.e., any provided via [`CoinSelectionSource::select_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + MaybeSend + 'a; +} + +impl CoinSelectionSource for C +where + C::Target: CoinSelectionSource, +{ + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future> + MaybeSend + 'a { + self.deref().select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ) + } + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + MaybeSend + 'a { + self.deref().sign_psbt(psbt) + } +} + +/// An alternative to [`CoinSelectionSource`] that can be implemented and used along [`Wallet`] to +/// provide a default implementation to [`CoinSelectionSource`]. +/// +/// For a synchronous version of this trait, see [`WalletSourceSync`]. +/// +/// This is not exported to bindings users as async is only supported in Rust. +// Note that updates to documentation on this trait should be copied to the synchronous version. +pub trait WalletSource { + /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. + fn list_confirmed_utxos<'a>( + &'a self, + ) -> impl Future, ()>> + MaybeSend + 'a; + + /// Returns the previous transaction containing the UTXO referenced by the outpoint. + fn get_prevtx<'a>( + &'a self, outpoint: OutPoint, + ) -> impl Future> + MaybeSend + 'a; + + /// Returns a script to use for change above dust resulting from a successful coin selection + /// attempt. + fn get_change_script<'a>( + &'a self, + ) -> impl Future> + MaybeSend + 'a; + + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within + /// the transaction known to the wallet (i.e., any provided via + /// [`WalletSource::list_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + MaybeSend + 'a; +} + +/// A wrapper over [`WalletSource`] that implements [`CoinSelectionSource`] by preferring UTXOs +/// that would avoid conflicting double spends. If not enough UTXOs are available to do so, +/// conflicting double spends may happen. +/// +/// For a synchronous version of this wrapper, see [`WalletSync`]. +/// +/// This is not exported to bindings users as async is only supported in Rust. +// Note that updates to documentation on this struct should be copied to the synchronous version. +pub struct Wallet +where + W::Target: WalletSource + MaybeSend, +{ + source: W, + logger: L, + // TODO: Do we care about cleaning this up once the UTXOs have a confirmed spend? We can do so + // by checking whether any UTXOs that exist in the map are no longer returned in + // `list_confirmed_utxos`. + locked_utxos: Mutex>>, +} + +impl Wallet +where + W::Target: WalletSource + MaybeSend, +{ + /// Returns a new instance backed by the given [`WalletSource`] that serves as an implementation + /// of [`CoinSelectionSource`]. + pub fn new(source: W, logger: L) -> Self { + Self { source, logger, locked_utxos: Mutex::new(new_hash_map()) } + } + + /// Performs coin selection on the set of UTXOs obtained from + /// [`WalletSource::list_confirmed_utxos`]. Its algorithm can be described as "smallest + /// above-dust-after-spend first", with a slight twist: we may skip UTXOs that are above dust at + /// the target feerate after having spent them in a separate claim transaction if + /// `force_conflicting_utxo_spend` is unset to avoid producing conflicting transactions. If + /// `tolerate_high_network_feerates` is set, we'll attempt to spend UTXOs that contribute at + /// least 1 satoshi at the current feerate, otherwise, we'll only attempt to spend those which + /// contribute at least twice their fee. + async fn select_confirmed_utxos_internal( + &self, utxos: &[Utxo], claim_id: Option, force_conflicting_utxo_spend: bool, + tolerate_high_network_feerates: bool, target_feerate_sat_per_1000_weight: u32, + preexisting_tx_weight: u64, input_amount_sat: Amount, target_amount_sat: Amount, + max_tx_weight: u64, + ) -> Result { + debug_assert!(!(claim_id.is_none() && force_conflicting_utxo_spend)); + + // P2WSH and P2TR outputs are both the heaviest-weight standard outputs at 34 bytes + let max_coin_selection_weight = max_tx_weight + .checked_sub(preexisting_tx_weight + P2WSH_TXOUT_WEIGHT) + .ok_or_else(|| { + log_debug!( + self.logger, + "max_tx_weight is too small to accommodate the preexisting tx weight plus a P2WSH/P2TR output" + ); + })?; + + let mut selected_amount; + let mut total_fees; + let mut selected_utxos; + { + let mut locked_utxos = self.locked_utxos.lock().unwrap(); + let mut eligible_utxos = utxos + .iter() + .filter_map(|utxo| { + if let Some(utxo_claim_id) = locked_utxos.get(&utxo.outpoint) { + // TODO(splicing): For splicing (i.e., claim_id.is_none()), ideally we'd + // allow force_conflicting_utxo_spend for an RBF attempt. However, we'd need + // something similar to a ClaimId to identify a splice. + if (utxo_claim_id.is_none() || claim_id.is_none()) + || (*utxo_claim_id != claim_id && !force_conflicting_utxo_spend) + { + log_trace!( + self.logger, + "Skipping UTXO {} to prevent conflicting spend", + utxo.outpoint + ); + return None; + } + } + let fee_to_spend_utxo = Amount::from_sat(fee_for_weight( + target_feerate_sat_per_1000_weight, + BASE_INPUT_WEIGHT + utxo.satisfaction_weight, + )); + let should_spend = if tolerate_high_network_feerates { + utxo.output.value > fee_to_spend_utxo + } else { + utxo.output.value >= fee_to_spend_utxo * 2 + }; + if should_spend { + Some((utxo, fee_to_spend_utxo)) + } else { + log_trace!( + self.logger, + "Skipping UTXO {} due to dust proximity after spend", + utxo.outpoint + ); + None + } + }) + .collect::>(); + eligible_utxos.sort_unstable_by_key(|(utxo, fee_to_spend_utxo)| { + utxo.output.value - *fee_to_spend_utxo + }); + + selected_amount = input_amount_sat; + total_fees = Amount::from_sat(fee_for_weight( + target_feerate_sat_per_1000_weight, + preexisting_tx_weight, + )); + selected_utxos = VecDeque::new(); + // Invariant: `selected_utxos_weight` is never greater than `max_coin_selection_weight` + let mut selected_utxos_weight = 0; + for (utxo, fee_to_spend_utxo) in eligible_utxos { + if selected_amount >= target_amount_sat + total_fees { + break; + } + // First skip any UTXOs with prohibitive satisfaction weights + if BASE_INPUT_WEIGHT + utxo.satisfaction_weight > max_coin_selection_weight { + continue; + } + // If adding this UTXO to `selected_utxos` would push us over the + // `max_coin_selection_weight`, remove UTXOs from the front to make room + // for this new UTXO. + while selected_utxos_weight + BASE_INPUT_WEIGHT + utxo.satisfaction_weight + > max_coin_selection_weight + && !selected_utxos.is_empty() + { + let (smallest_value_after_spend_utxo, fee_to_spend_utxo): (Utxo, Amount) = + selected_utxos.pop_front().unwrap(); + selected_amount -= smallest_value_after_spend_utxo.output.value; + total_fees -= fee_to_spend_utxo; + selected_utxos_weight -= + BASE_INPUT_WEIGHT + smallest_value_after_spend_utxo.satisfaction_weight; + } + selected_amount += utxo.output.value; + total_fees += fee_to_spend_utxo; + selected_utxos_weight += BASE_INPUT_WEIGHT + utxo.satisfaction_weight; + selected_utxos.push_back((utxo.clone(), fee_to_spend_utxo)); + } + if selected_amount < target_amount_sat + total_fees { + log_debug!( + self.logger, + "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", + target_feerate_sat_per_1000_weight, + max_coin_selection_weight, + ); + return Err(()); + } + // Once we've selected enough UTXOs to cover `target_amount_sat + total_fees`, + // we may be able to remove some small-value ones while still covering + // `target_amount_sat + total_fees`. + while !selected_utxos.is_empty() + && selected_amount - selected_utxos.front().unwrap().0.output.value + >= target_amount_sat + total_fees - selected_utxos.front().unwrap().1 + { + let (smallest_value_after_spend_utxo, fee_to_spend_utxo) = + selected_utxos.pop_front().unwrap(); + selected_amount -= smallest_value_after_spend_utxo.output.value; + total_fees -= fee_to_spend_utxo; + } + for (utxo, _) in &selected_utxos { + locked_utxos.insert(utxo.outpoint, claim_id); + } + } + + let remaining_amount = selected_amount - target_amount_sat - total_fees; + let change_script = self.source.get_change_script().await?; + let change_output_fee = fee_for_weight( + target_feerate_sat_per_1000_weight, + (8 /* value */ + change_script.consensus_encode(&mut sink()).unwrap() as u64) + * WITNESS_SCALE_FACTOR as u64, + ); + let change_output_amount = + Amount::from_sat(remaining_amount.to_sat().saturating_sub(change_output_fee)); + let change_output = if change_output_amount < change_script.minimal_non_dust() { + log_debug!(self.logger, "Coin selection attempt did not yield change output"); + None + } else { + Some(TxOut { script_pubkey: change_script, value: change_output_amount }) + }; + + let mut confirmed_utxos = Vec::with_capacity(selected_utxos.len()); + for (utxo, _) in selected_utxos { + let prevtx = self.source.get_prevtx(utxo.outpoint).await?; + let prevtx_id = prevtx.compute_txid(); + if prevtx_id != utxo.outpoint.txid + || prevtx.output.get(utxo.outpoint.vout as usize).is_none() + { + log_error!( + self.logger, + "Tx {} from wallet source doesn't contain output referenced by outpoint: {}", + prevtx_id, + utxo.outpoint, + ); + return Err(()); + } + + confirmed_utxos.push(ConfirmedUtxo { utxo, prevtx }); + } + + Ok(CoinSelection { confirmed_utxos, change_output }) + } +} + +impl CoinSelectionSource + for Wallet +where + W::Target: WalletSource + MaybeSend + MaybeSync, +{ + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future> + MaybeSend + 'a { + async move { + let utxos = self.source.list_confirmed_utxos().await?; + // TODO: Use fee estimation utils when we upgrade to bitcoin v0.30.0. + let total_output_size: u64 = must_pay_to + .iter() + .map( + |output| 8 /* value */ + 1 /* script len */ + output.script_pubkey.len() as u64, + ) + .sum(); + let total_satisfaction_weight: u64 = + must_spend.iter().map(|input| input.satisfaction_weight).sum(); + let total_input_weight = + (BASE_INPUT_WEIGHT * must_spend.len() as u64) + total_satisfaction_weight; + + let preexisting_tx_weight = SEGWIT_MARKER_FLAG_WEIGHT + + total_input_weight + + ((BASE_TX_SIZE + total_output_size) * WITNESS_SCALE_FACTOR as u64); + let input_amount_sat = must_spend.iter().map(|input| input.previous_utxo.value).sum(); + let target_amount_sat = must_pay_to.iter().map(|output| output.value).sum(); + + let configs = [(false, false), (false, true), (true, false), (true, true)]; + for (force_conflicting_utxo_spend, tolerate_high_network_feerates) in configs { + if claim_id.is_none() && force_conflicting_utxo_spend { + continue; + } + log_debug!( + self.logger, + "Attempting coin selection targeting {} sat/kW (force_conflicting_utxo_spend = {}, tolerate_high_network_feerates = {})", + target_feerate_sat_per_1000_weight, + force_conflicting_utxo_spend, + tolerate_high_network_feerates + ); + let attempt = self + .select_confirmed_utxos_internal( + &utxos, + claim_id, + force_conflicting_utxo_spend, + tolerate_high_network_feerates, + target_feerate_sat_per_1000_weight, + preexisting_tx_weight, + input_amount_sat, + target_amount_sat, + max_tx_weight, + ) + .await; + if attempt.is_ok() { + return attempt; + } + } + Err(()) + } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + MaybeSend + 'a { + self.source.sign_psbt(psbt) + } +} + +/// An alternative to [`CoinSelectionSourceSync`] that can be implemented and used along +/// [`WalletSync`] to provide a default implementation to [`CoinSelectionSourceSync`]. +/// +/// For an asynchronous version of this trait, see [`WalletSource`]. +// Note that updates to documentation on this trait should be copied to the asynchronous version. +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 referenced by the outpoint. + fn get_prevtx(&self, outpoint: OutPoint) -> 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`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + fn sign_psbt(&self, psbt: Psbt) -> Result; +} + +struct WalletSourceSyncWrapper(T) +where + T::Target: WalletSourceSync; + +// Implement `Deref` directly on WalletSourceSyncWrapper so that it can be used directly +// below, rather than via a wrapper. +impl Deref for WalletSourceSyncWrapper +where + T::Target: WalletSourceSync, +{ + type Target = Self; + fn deref(&self) -> &Self { + self + } +} + +impl WalletSource for WalletSourceSyncWrapper +where + T::Target: WalletSourceSync, +{ + fn list_confirmed_utxos<'a>( + &'a self, + ) -> impl Future, ()>> + MaybeSend + 'a { + let utxos = self.0.list_confirmed_utxos(); + async move { utxos } + } + + fn get_prevtx<'a>( + &'a self, outpoint: OutPoint, + ) -> impl Future> + MaybeSend + 'a { + let prevtx = self.0.get_prevtx(outpoint); + Box::pin(async move { prevtx }) + } + + fn get_change_script<'a>( + &'a self, + ) -> impl Future> + MaybeSend + 'a { + let script = self.0.get_change_script(); + async move { script } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + MaybeSend + 'a { + let signed_psbt = self.0.sign_psbt(psbt); + async move { signed_psbt } + } +} + +/// A wrapper over [`WalletSourceSync`] that implements [`CoinSelectionSourceSync`] by preferring +/// UTXOs that would avoid conflicting double spends. If not enough UTXOs are available to do so, +/// conflicting double spends may happen. +/// +/// For an asynchronous version of this wrapper, see [`Wallet`]. +// Note that updates to documentation on this struct should be copied to the asynchronous version. +pub struct WalletSync +where + W::Target: WalletSourceSync + MaybeSend, +{ + wallet: Wallet, L>, +} + +impl WalletSync +where + W::Target: WalletSourceSync + MaybeSend, +{ + /// Constructs a new [`WalletSync`] instance. + pub fn new(source: W, logger: L) -> Self { + Self { wallet: Wallet::new(WalletSourceSyncWrapper(source), logger) } + } +} + +impl CoinSelectionSourceSync + for WalletSync +where + W::Target: WalletSourceSync + MaybeSend + MaybeSync, +{ + fn select_confirmed_utxos( + &self, claim_id: Option, must_spend: Vec, must_pay_to: &[TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> Result { + let fut = self.wallet.select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ); + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match pin!(fut).poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + unreachable!( + "Wallet::select_confirmed_utxos should not be pending in a sync context" + ); + }, + } + } + + fn sign_psbt(&self, psbt: Psbt) -> Result { + let fut = self.wallet.sign_psbt(psbt); + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match pin!(fut).poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + unreachable!("Wallet::sign_psbt should not be pending in a sync context"); + }, + } + } +} + +/// 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 +/// [`WalletSourceSync`], which can provide a default implementation of this trait when used with +/// [`WalletSync`]. +/// +/// For an asynchronous version of this trait, see [`CoinSelectionSource`]. +// Note that updates to documentation on this trait should be copied to the asynchronous version. +pub trait CoinSelectionSourceSync { + /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are + /// available to spend. Implementations are free to pick their coin selection algorithm of + /// choice, as long as the following requirements are met: + /// + /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction + /// throughout coin selection, but must not be returned as part of the result. + /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction + /// throughout coin selection. In some cases, like when funding an anchor transaction, this + /// set is empty. Implementations should ensure they handle this correctly on their end, + /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be + /// provided, in which case a zero-value empty OP_RETURN output can be used instead. + /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the + /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. + /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this + /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC + /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for + /// anchor transactions, we will try your coin selection again with the same input-output + /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions + /// cannot be downsized. + /// + /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of + /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require + /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and + /// delaying block inclusion. + /// + /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they + /// can be re-used within new fee-bumped iterations of the original claiming transaction, + /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a + /// transaction associated with it, and all of the available UTXOs have already been assigned to + /// other claims, implementations must be willing to double spend their UTXOs. The choice of + /// which UTXOs to double spend is left to the implementation, but it must strive to keep the + /// set of other claims being double spent to a minimum. + /// + /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims + fn select_confirmed_utxos( + &self, claim_id: Option, must_spend: Vec, must_pay_to: &[TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> Result; + + /// Signs and provides the full witness for all inputs within the transaction known to the + /// trait (i.e., any provided via [`CoinSelectionSourceSync::select_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + fn sign_psbt(&self, psbt: Psbt) -> Result; +} + +impl CoinSelectionSourceSync for C +where + C::Target: CoinSelectionSourceSync, +{ + fn select_confirmed_utxos( + &self, claim_id: Option, must_spend: Vec, must_pay_to: &[TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> Result { + self.deref().select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ) + } + fn sign_psbt(&self, psbt: Psbt) -> Result { + self.deref().sign_psbt(psbt) + } +} + +pub(crate) struct CoinSelectionSourceSyncWrapper(pub(crate) T); + +impl CoinSelectionSource for CoinSelectionSourceSyncWrapper { + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option, must_spend: Vec, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future> + MaybeSend + 'a { + let coins = self.0.select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ); + async move { coins } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future> + MaybeSend + 'a { + let psbt = self.0.sign_psbt(psbt); + async move { psbt } + } +}