diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d40f72f4a..28eae2ab2 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -174,8 +174,14 @@ interface Node { [Throws=NodeError] UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config); [Throws=NodeError] + UserChannelId open_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + [Throws=NodeError] + UserChannelId open_announced_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + [Throws=NodeError] void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats); [Throws=NodeError] + void splice_in_with_all([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id); + [Throws=NodeError] void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats); [Throws=NodeError] void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id); diff --git a/src/event.rs b/src/event.rs index 5f02f9f87..01b62425a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1161,52 +1161,45 @@ where } let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); - if anchor_channel { - if let Some(anchor_channels_config) = - self.config.anchor_channels_config.as_ref() - { - let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats( - &self.channel_manager, - &self.config, - ); - let spendable_amount_sats = self - .wallet - .get_spendable_amount_sats(cur_anchor_reserve_sats) - .unwrap_or(0); - - let required_amount_sats = if anchor_channels_config - .trusted_peers_no_reserve - .contains(&counterparty_node_id) - { - 0 - } else { - anchor_channels_config.per_channel_reserve_sats - }; + if anchor_channel && self.config.anchor_channels_config.is_none() { + log_error!( + self.logger, + "Rejecting inbound channel from peer {} due to Anchor channels being disabled.", + counterparty_node_id, + ); + self.channel_manager + .force_close_broadcasting_latest_txn( + &temporary_channel_id, + &counterparty_node_id, + "Channel request rejected".to_string(), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return Ok(()); + } - if spendable_amount_sats < required_amount_sats { - log_error!( - self.logger, - "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves. Available: {}/{}sats", - counterparty_node_id, - spendable_amount_sats, - required_amount_sats, - ); - self.channel_manager - .force_close_broadcasting_latest_txn( - &temporary_channel_id, - &counterparty_node_id, - "Channel request rejected".to_string(), - ) - .unwrap_or_else(|e| { - log_error!(self.logger, "Failed to reject channel: {:?}", e) - }); - return Ok(()); - } - } else { + let required_reserve_sats = crate::new_channel_anchor_reserve_sats( + &self.config, + &counterparty_node_id, + anchor_channel, + ); + + if required_reserve_sats > 0 { + let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats( + &self.channel_manager, + &self.config, + ); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + + if spendable_amount_sats < required_reserve_sats { log_error!( self.logger, - "Rejecting inbound channel from peer {} due to Anchor channels being disabled.", + "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves. Available: {}/{}sats", counterparty_node_id, + spendable_amount_sats, + required_reserve_sats, ); self.channel_manager .force_close_broadcasting_latest_txn( diff --git a/src/lib.rs b/src/lib.rs index 1b93cb6e9..64bce2454 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,13 +139,14 @@ use graph::NetworkGraph; use io::utils::write_node_metrics; use lightning::chain::BestBlock; use lightning::impl_writeable_tlv_based; +use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; use lightning::sign::EntropySource; use lightning::util::persist::KVStoreSync; -use lightning::util::wallet_utils::Wallet as LdkWallet; +use lightning::util::wallet_utils::{Input, Wallet as LdkWallet}; use lightning_background_processor::process_events_async; use liquidity::{LSPS1Liquidity, LiquiditySource}; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; @@ -169,6 +170,7 @@ pub use { }; use crate::scoring::setup_background_pathfinding_scores_sync; +use crate::wallet::FundingAmount; #[cfg(feature = "uniffi")] uniffi::include_scaffolding!("ldk_node"); @@ -1092,7 +1094,7 @@ impl Node { } fn open_channel_inner( - &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, + &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: FundingAmount, push_to_counterparty_msat: Option, channel_config: Option, announce_for_forwarding: bool, ) -> Result { @@ -1112,8 +1114,38 @@ impl Node { con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; - // Check funds availability after connection (includes anchor reserve calculation) - self.check_sufficient_funds_for_channel(channel_amount_sats, &node_id)?; + let channel_amount_sats = match channel_amount_sats { + FundingAmount::Exact { amount_sats } => { + // Check funds availability after connection (includes anchor reserve + // calculation). + self.check_sufficient_funds_for_channel(amount_sats, &peer_info.node_id)?; + amount_sats + }, + FundingAmount::Max => { + // Determine max funding amount from all available on-chain funds. + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let new_channel_reserve = + self.new_channel_anchor_reserve_sats(&peer_info.node_id)?; + let total_anchor_reserve_sats = cur_anchor_reserve_sats + new_channel_reserve; + + let fee_rate = + self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding); + + let amount = + self.wallet.get_max_funding_amount(total_anchor_reserve_sats, fee_rate)?; + + log_info!( + self.logger, + "Opening channel with all balance: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)", + amount, + fee_rate.to_sat_per_kwu(), + total_anchor_reserve_sats, + ); + + amount + }, + }; let mut user_config = default_user_config(&self.config); user_config.channel_handshake_config.announce_for_forwarding = announce_for_forwarding; @@ -1156,6 +1188,16 @@ impl Node { } } + fn new_channel_anchor_reserve_sats(&self, peer_node_id: &PublicKey) -> Result { + let init_features = self + .peer_manager + .peer_by_node_id(peer_node_id) + .ok_or(Error::ConnectionFailed)? + .init_features; + let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx(); + Ok(new_channel_anchor_reserve_sats(&self.config, peer_node_id, anchor_channel)) + } + fn check_sufficient_funds_for_channel( &self, amount_sats: u64, peer_node_id: &PublicKey, ) -> Result<(), Error> { @@ -1174,21 +1216,8 @@ impl Node { } // Fail if we have less than the channel value + anchor reserve available (if applicable). - let init_features = self - .peer_manager - .peer_by_node_id(peer_node_id) - .ok_or(Error::ConnectionFailed)? - .init_features; - let required_funds_sats = amount_sats - + self.config.anchor_channels_config.as_ref().map_or(0, |c| { - if init_features.requires_anchors_zero_fee_htlc_tx() - && !c.trusted_peers_no_reserve.contains(peer_node_id) - { - c.per_channel_reserve_sats - } else { - 0 - } - }); + let required_funds_sats = + amount_sats + self.new_channel_anchor_reserve_sats(peer_node_id)?; if spendable_amount_sats < required_funds_sats { log_error!(self.logger, @@ -1225,7 +1254,7 @@ impl Node { self.open_channel_inner( node_id, address, - channel_amount_sats, + FundingAmount::Exact { amount_sats: channel_amount_sats }, push_to_counterparty_msat, channel_config, false, @@ -1265,36 +1294,145 @@ impl Node { self.open_channel_inner( node_id, address, - channel_amount_sats, + FundingAmount::Exact { amount_sats: channel_amount_sats }, push_to_counterparty_msat, channel_config, true, ) } - /// Add funds from the on-chain wallet into an existing channel. + /// Connect to a node and open a new unannounced channel, using all available on-chain funds + /// minus fees and anchor reserves. /// - /// This provides for increasing a channel's outbound liquidity without re-balancing or closing - /// it. Once negotiation with the counterparty is complete, the channel remains operational - /// while waiting for a new funding transaction to confirm. + /// To open an announced channel, see [`Node::open_announced_channel_with_all`]. /// - /// # Experimental API + /// Disconnects and reconnects are handled automatically. /// - /// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but - /// this classification may change in the future. - pub fn splice_in( + /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the + /// channel counterparty on channel open. This can be useful to start out with the balance not + /// entirely shifted to one side, therefore allowing to receive payments from the getgo. + /// + /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. + /// + /// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats + pub fn open_channel_with_all( + &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, + channel_config: Option, + ) -> Result { + self.open_channel_inner( + node_id, + address, + FundingAmount::Max, + push_to_counterparty_msat, + channel_config, + false, + ) + } + + /// Connect to a node and open a new announced channel, using all available on-chain funds + /// minus fees and anchor reserves. + /// + /// This will return an error if the node has not been sufficiently configured to operate as a + /// forwarding node that can properly announce its existence to the public network graph, i.e., + /// [`Config::listening_addresses`] and [`Config::node_alias`] are unset. + /// + /// To open an unannounced channel, see [`Node::open_channel_with_all`]. + /// + /// Disconnects and reconnects are handled automatically. + /// + /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the + /// channel counterparty on channel open. This can be useful to start out with the balance not + /// entirely shifted to one side, therefore allowing to receive payments from the getgo. + /// + /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. + /// + /// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats + pub fn open_announced_channel_with_all( + &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, + channel_config: Option, + ) -> Result { + if let Err(err) = may_announce_channel(&self.config) { + log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {err}"); + return Err(Error::ChannelCreationFailed); + } + + self.open_channel_inner( + node_id, + address, + FundingAmount::Max, + push_to_counterparty_msat, + channel_config, + true, + ) + } + + fn splice_in_inner( &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, - splice_amount_sats: u64, + splice_amount_sats: FundingAmount, ) -> Result<(), Error> { let open_channels = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { - self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?; - let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding); + let splice_amount_sats = match splice_amount_sats { + FundingAmount::Exact { amount_sats } => amount_sats, + FundingAmount::Max => { + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + + const EMPTY_SCRIPT_SIG_WEIGHT: u64 = + 1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64; + + let funding_txo = channel_details.funding_txo.ok_or_else(|| { + log_error!(self.logger, "Failed to splice channel: channel not yet ready",); + Error::ChannelSplicingFailed + })?; + + let funding_output = channel_details.get_funding_output().ok_or_else(|| { + log_error!(self.logger, "Failed to splice channel: channel not yet ready"); + Error::ChannelSplicingFailed + })?; + + let shared_input = Input { + outpoint: funding_txo.into_bitcoin_outpoint(), + previous_utxo: funding_output.clone(), + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + + FUNDING_TRANSACTION_WITNESS_WEIGHT, + }; + + let amount = self + .wallet + .get_max_splice_in_amount( + shared_input, + funding_output.script_pubkey.clone(), + cur_anchor_reserve_sats, + fee_rate, + ) + .map_err(|e| { + log_error!( + self.logger, + "Failed to determine max splice-in amount: {e:?}" + ); + e + })?; + + log_info!( + self.logger, + "Splicing in with all balance: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)", + amount, + fee_rate.to_sat_per_kwu(), + cur_anchor_reserve_sats, + ); + + amount + }, + }; + + self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?; + let funding_template = self .channel_manager .splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate) @@ -1336,6 +1474,46 @@ impl Node { } } + /// Add funds to an existing channel from a transaction output you control. + /// + /// This provides for increasing a channel's outbound liquidity without re-balancing or closing + /// it. Once negotiation with the counterparty is complete, the channel remains operational + /// while waiting for a new funding transaction to confirm. + /// + /// # Experimental API + /// + /// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but + /// this classification may change in the future. + pub fn splice_in( + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, + splice_amount_sats: u64, + ) -> Result<(), Error> { + self.splice_in_inner( + user_channel_id, + counterparty_node_id, + FundingAmount::Exact { amount_sats: splice_amount_sats }, + ) + } + + /// Add all available on-chain funds into an existing channel. + /// + /// This is similar to [`Node::splice_in`] but uses all available confirmed on-chain funds + /// instead of requiring a specific amount. + /// + /// This provides for increasing a channel's outbound liquidity without re-balancing or closing + /// it. Once negotiation with the counterparty is complete, the channel remains operational + /// while waiting for a new funding transaction to confirm. + /// + /// # Experimental API + /// + /// This API is experimental. Currently, a splice-in will be marked as an outbound payment, but + /// this classification may change in the future. + pub fn splice_in_with_all( + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, + ) -> Result<(), Error> { + self.splice_in_inner(user_channel_id, counterparty_node_id, FundingAmount::Max) + } + /// Remove funds from an existing channel, sending them to an on-chain address. /// /// This provides for decreasing a channel's outbound liquidity without re-balancing or closing @@ -1823,3 +2001,19 @@ pub(crate) fn total_anchor_channels_reserve_sats( * anchor_channels_config.per_channel_reserve_sats }) } + +pub(crate) fn new_channel_anchor_reserve_sats( + config: &Config, peer_node_id: &PublicKey, anchor_channel: bool, +) -> u64 { + if !anchor_channel { + return 0; + } + + config.anchor_channels_config.as_ref().map_or(0, |c| { + if c.trusted_peers_no_reserve.contains(peer_node_id) { + 0 + } else { + c.per_channel_reserve_sats + } + }) +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index dd88dad90..20b96c747 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -68,9 +68,16 @@ pub(crate) enum OnchainSendAmount { AllDrainingReserve, } +pub(crate) enum FundingAmount { + Exact { amount_sats: u64 }, + Max, +} + pub(crate) mod persist; pub(crate) mod ser; +const DUST_LIMIT_SATS: u64 = 546; + pub(crate) struct Wallet { // A BDK on-chain wallet. inner: Mutex>, @@ -533,6 +540,158 @@ impl Wallet { self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) } + fn build_drain_psbt( + &self, locked_wallet: &mut PersistedWallet, + drain_script: ScriptBuf, cur_anchor_reserve_sats: u64, fee_rate: FeeRate, + shared_input: Option<&Input>, + ) -> Result { + let anchor_address = if cur_anchor_reserve_sats > DUST_LIMIT_SATS { + Some(locked_wallet.peek_address(KeychainKind::Internal, 0)) + } else { + None + }; + + let mut tx_builder = locked_wallet.build_tx(); + tx_builder.drain_wallet().drain_to(drain_script).fee_rate(fee_rate); + + if let Some(address_info) = anchor_address { + tx_builder.add_recipient( + address_info.address.script_pubkey(), + Amount::from_sat(cur_anchor_reserve_sats), + ); + } + + if let Some(input) = shared_input { + let psbt_input = psbt::Input { + witness_utxo: Some(input.previous_utxo.clone()), + ..Default::default() + }; + let weight = Weight::from_wu(input.satisfaction_weight); + tx_builder.only_witness_utxo().exclude_unconfirmed(); + tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|e| { + log_error!(self.logger, "Failed to add shared input for fee estimation: {e}"); + Error::ChannelSplicingFailed + })?; + } + + let psbt = tx_builder.finish().map_err(|err| { + log_error!(self.logger, "Failed to create temporary drain transaction: {err}"); + err + })?; + + Ok(psbt) + } + + /// Builds a temporary drain transaction and returns the maximum amount that would be sent to + /// the drain output, along with the PSBT for further inspection. + /// + /// The caller is responsible for cancelling the PSBT via `locked_wallet.cancel_tx()`. + fn get_max_drain_amount( + &self, locked_wallet: &mut PersistedWallet, + drain_script: ScriptBuf, cur_anchor_reserve_sats: u64, fee_rate: FeeRate, + shared_input: Option<&Input>, + ) -> Result<(u64, Psbt), Error> { + let balance = locked_wallet.balance(); + let spendable_amount_sats = + self.get_balances_inner(balance, cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0); + + if spendable_amount_sats == 0 { + log_error!( + self.logger, + "Unable to determine max amount: no spendable funds available." + ); + return Err(Error::InsufficientFunds); + } + + let tmp_psbt = self.build_drain_psbt( + locked_wallet, + drain_script.clone(), + cur_anchor_reserve_sats, + fee_rate, + shared_input, + )?; + + let drain_output_value = tmp_psbt + .unsigned_tx + .output + .iter() + .find(|o| o.script_pubkey == drain_script) + .map(|o| o.value) + .ok_or_else(|| { + log_error!(self.logger, "Failed to find drain output in temporary transaction"); + Error::InsufficientFunds + })?; + + let shared_input_value = shared_input.map(|i| i.previous_utxo.value.to_sat()).unwrap_or(0); + + let max_amount = drain_output_value.to_sat().saturating_sub(shared_input_value); + + if max_amount < DUST_LIMIT_SATS { + log_error!( + self.logger, + "Unable to proceed: available funds would be consumed entirely by fees. \ + Available: {spendable_amount_sats}sats, drain output: {}sats.", + drain_output_value.to_sat(), + ); + return Err(Error::InsufficientFunds); + } + + Ok((max_amount, tmp_psbt)) + } + + /// Returns the maximum amount available for funding a channel, accounting for on-chain fees + /// and anchor reserves. + pub(crate) fn get_max_funding_amount( + &self, cur_anchor_reserve_sats: u64, fee_rate: FeeRate, + ) -> Result { + let mut locked_wallet = self.inner.lock().unwrap(); + + // Use a dummy P2WSH script (34 bytes) to match the size of a real funding output. + let dummy_p2wsh_script = ScriptBuf::new().to_p2wsh(); + + let (max_amount, tmp_psbt) = self.get_max_drain_amount( + &mut locked_wallet, + dummy_p2wsh_script, + cur_anchor_reserve_sats, + fee_rate, + None, + )?; + + locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx); + + Ok(max_amount) + } + + /// Returns the maximum amount available for splicing into an existing channel, accounting for + /// on-chain fees and anchor reserves, along with the wallet UTXOs to use as inputs. + pub(crate) fn get_max_splice_in_amount( + &self, shared_input: Input, shared_output_script: ScriptBuf, cur_anchor_reserve_sats: u64, + fee_rate: FeeRate, + ) -> Result { + let mut locked_wallet = self.inner.lock().unwrap(); + + debug_assert!(matches!( + locked_wallet.public_descriptor(KeychainKind::External), + ExtendedDescriptor::Wpkh(_) + )); + debug_assert!(matches!( + locked_wallet.public_descriptor(KeychainKind::Internal), + ExtendedDescriptor::Wpkh(_) + )); + + let (splice_amount, tmp_psbt) = self.get_max_drain_amount( + &mut locked_wallet, + shared_output_script, + cur_anchor_reserve_sats, + fee_rate, + Some(&shared_input), + )?; + + locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx); + + Ok(splice_amount) + } + pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result { Address::::from_str(address.to_string().as_str()) .map_err(|_| Error::InvalidAddress)? @@ -556,7 +715,6 @@ impl Wallet { let mut locked_wallet = self.inner.lock().unwrap(); // Prepare the tx_builder. We properly check the reserve requirements (again) further down. - const DUST_LIMIT_SATS: u64 = 546; let tx_builder = match send_amount { OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => { let mut tx_builder = locked_wallet.build_tx(); @@ -567,63 +725,29 @@ impl Wallet { OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } if cur_anchor_reserve_sats > DUST_LIMIT_SATS => { - let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0); - let balance = locked_wallet.balance(); - let spendable_amount_sats = self - .get_balances_inner(balance, cur_anchor_reserve_sats) - .map(|(_, s)| s) - .unwrap_or(0); - let tmp_tx = { - let mut tmp_tx_builder = locked_wallet.build_tx(); - tmp_tx_builder - .drain_wallet() - .drain_to(address.script_pubkey()) - .add_recipient( - change_address_info.address.script_pubkey(), - Amount::from_sat(cur_anchor_reserve_sats), - ) - .fee_rate(fee_rate); - match tmp_tx_builder.finish() { - Ok(psbt) => psbt.unsigned_tx, - Err(err) => { - log_error!( - self.logger, - "Failed to create temporary transaction: {}", - err - ); - return Err(err.into()); - }, - } - }; + let (max_amount, tmp_psbt) = self.get_max_drain_amount( + &mut locked_wallet, + address.script_pubkey(), + cur_anchor_reserve_sats, + fee_rate, + None, + )?; - let estimated_tx_fee = locked_wallet.calculate_fee(&tmp_tx).map_err(|e| { - log_error!( - self.logger, - "Failed to calculate fee of temporary transaction: {}", + let estimated_tx_fee = + locked_wallet.calculate_fee(&tmp_psbt.unsigned_tx).map_err(|e| { + log_error!( + self.logger, + "Failed to calculate fee of temporary transaction: {}", + e + ); e - ); - e - })?; - - // 'cancel' the transaction to free up any used change addresses - locked_wallet.cancel_tx(&tmp_tx); - - let estimated_spendable_amount = Amount::from_sat( - spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat()), - ); + })?; - if estimated_spendable_amount == Amount::ZERO { - log_error!(self.logger, - "Unable to send payment without infringing on Anchor reserves. Available: {}sats, estimated fee required: {}sats.", - spendable_amount_sats, - estimated_tx_fee, - ); - return Err(Error::InsufficientFunds); - } + locked_wallet.cancel_tx(&tmp_psbt.unsigned_tx); let mut tx_builder = locked_wallet.build_tx(); tx_builder - .add_recipient(address.script_pubkey(), estimated_spendable_amount) + .add_recipient(address.script_pubkey(), Amount::from_sat(max_amount)) .fee_absolute(estimated_tx_fee); tx_builder }, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c75a6947c..ee9d267fe 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -32,6 +32,7 @@ use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, + UserChannelId, }; use lightning::io; use lightning::ln::msgs::SocketAddress; @@ -747,6 +748,48 @@ pub async fn open_channel_push_amt( funding_txo_a } +pub async fn open_channel_with_all( + node_a: &TestNode, node_b: &TestNode, should_announce: bool, electrsd: &ElectrsD, +) -> OutPoint { + if should_announce { + node_a + .open_announced_channel_with_all( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + None, + None, + ) + .unwrap(); + } else { + node_a + .open_channel_with_all( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + None, + None, + ) + .unwrap(); + } + assert!(node_a.list_peers().iter().find(|c| { c.node_id == node_b.node_id() }).is_some()); + + let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id()); + let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id()); + assert_eq!(funding_txo_a, funding_txo_b); + wait_for_tx(&electrsd.client, funding_txo_a.txid).await; + + funding_txo_a +} + +pub async fn splice_in_with_all( + node_a: &TestNode, node_b: &TestNode, user_channel_id: &UserChannelId, electrsd: &ElectrsD, +) { + node_a.splice_in_with_all(user_channel_id, node_b.node_id()).unwrap(); + + let splice_txo = expect_splice_pending_event!(node_a, node_b.node_id()); + expect_splice_pending_event!(node_b, node_a.node_id()); + wait_for_tx(&electrsd.client, splice_txo.txid).await; +} + pub(crate) async fn do_channel_full_cycle( node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool, expect_anchor_channel: bool, force_close: bool, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 61c9c8281..5eb5a08af 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -21,10 +21,10 @@ use common::{ expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events, expect_event, expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait, - open_channel, open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf, - random_chain_source, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, - setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestStoreType, - TestSyncStore, + open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds, + premine_blocks, prepare_rbf, random_chain_source, random_config, random_listening_addresses, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all, + wait_for_tx, TestChainSource, TestStoreType, TestSyncStore, }; use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::entropy::NodeEntropy; @@ -2627,3 +2627,185 @@ async fn onchain_fee_bump_rbf() { assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000)); assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn open_channel_with_all_with_anchors() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + // After opening a channel with all balance, the remaining on-chain balance should only + // be the anchor reserve (25k sats by default) plus a small margin for change + let anchor_reserve_sat = 25_000; + let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; + assert!( + remaining_balance < anchor_reserve_sat + 500, + "Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}" + ); + + // Verify a channel was opened with most of the funds + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + let channel = &channels[0]; + assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500); + assert_eq!(channel.counterparty_node_id, node_b.node_id()); + assert_eq!(channel.funding_txo.unwrap(), funding_txo); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn open_channel_with_all_without_anchors() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + // Without anchors, there should be no remaining balance + let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; + assert_eq!( + remaining_balance, 0, + "Remaining balance {remaining_balance} should be zero without anchor reserve" + ); + + // Verify a channel was opened with all the funds accounting for fees + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + let channel = &channels[0]; + assert!(channel.channel_value_sats > premine_amount_sat - 500); + assert_eq!(channel.counterparty_node_id, node_b.node_id()); + assert_eq!(channel.funding_txo.unwrap(), funding_txo); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn splice_in_with_all_balance() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 5_000_000; + let channel_amount_sat = 1_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + // Open a channel with a fixed amount first + let funding_txo = open_channel(&node_a, &node_b, channel_amount_sat, false, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + assert_eq!(channels[0].channel_value_sats, channel_amount_sat); + assert_eq!(channels[0].funding_txo.unwrap(), funding_txo); + + let balance_before_splice = node_a.list_balances().spendable_onchain_balance_sats; + assert!(balance_before_splice > 0); + + // Splice in with all remaining on-chain funds + splice_in_with_all(&node_a, &node_b, &user_channel_id_a, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let _user_channel_id_a2 = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b2 = expect_channel_ready_event!(node_b, node_a.node_id()); + + // After splicing with all balance, channel value should be close to the premined amount + // minus fees and anchor reserve + let anchor_reserve_sat = 25_000; + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + let channel = &channels[0]; + assert!( + channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 1000, + "Channel value {} should be close to premined amount {} minus anchor reserve {} and fees", + channel.channel_value_sats, + premine_amount_sat, + anchor_reserve_sat, + ); + + // Remaining on-chain balance should be close to just the anchor reserve + let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; + assert!( + remaining_balance < anchor_reserve_sat + 500, + "Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}" + ); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +}