diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f8f563c7c..cfdc5701a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ `Event::PaymentClaimable`. ## Feature and API updates +- All `Node::open_channel` variants now take an optional `FeeRate` (after + `push_to_counterparty_msat`) that, when set, is used for the channel's funding transaction instead of the internal fee estimation. For the + `_with_all` variants it also determines the rate at which the maximum funding amount is + computed. When unset, behavior is unchanged. - Language-binding `Mnemonic` objects can be generated or constructed from entropy and expose their words, word indices, word count, entropy, checksum, and passphrase-derived seed. - `Node::list_payments` is now paginated: it takes an optional `PageToken` and returns a diff --git a/README.md b/README.md index b9d32c0a0c..b1d663ef67 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ fn main() { let node_id = PublicKey::from_str("NODE_ID").unwrap(); let node_addr = SocketAddress::from_str("IP_ADDR:PORT").unwrap(); - node.open_channel(node_id, node_addr, 10000, None, None).unwrap(); + node.open_channel(node_id, node_addr, 10000, None, None, None).unwrap(); let event = node.wait_next_event(); println!("EVENT: {:?}", event); diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt index 96cccfa96e..6c23e58850 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt @@ -253,7 +253,7 @@ class LibraryTest { assertEquals(100000uL, totalBalance1) assertEquals(100000uL, totalBalance2) - node1.openChannel(nodeId2, listenAddress2, 50000u, null, null) + node1.openChannel(nodeId2, listenAddress2, 50000u, null, null, null) val channelPendingEvent1 = node1.waitNextEvent() println("Got event: $channelPendingEvent1") diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index df6aa18f8d..259ae41d55 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -78,17 +78,17 @@ interface Node { [Throws=NodeError] void disconnect(PublicKey node_id); [Throws=NodeError] - UserChannelId open_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + UserChannelId open_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config); [Throws=NodeError] - UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config); [Throws=NodeError] - UserChannelId open_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + UserChannelId open_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config); [Throws=NodeError] - UserChannelId open_announced_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + UserChannelId open_announced_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config); [Throws=NodeError] - UserChannelId open_0reserve_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + UserChannelId open_0reserve_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config); [Throws=NodeError] - UserChannelId open_0reserve_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config); + UserChannelId open_0reserve_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, FeeRate? fee_rate, ChannelConfig? channel_config); [Throws=NodeError] void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats); [Throws=NodeError] diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 11699da0af..6f785fbaa0 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -159,7 +159,7 @@ def fund_nodes(node_1, node_2, esplora_endpoint, amount_sats=100000): node_2.sync_wallets() def open_channel_and_wait_ready(node_1, node_2, node_id_2, listening_address_2, esplora_endpoint, channel_amount_sats=50000): - node_1.open_channel(node_id_2, listening_address_2, channel_amount_sats, None, None) + node_1.open_channel(node_id_2, listening_address_2, channel_amount_sats, None, None, None) channel_pending_event_1 = expect_event(node_1, Event.CHANNEL_PENDING) expect_event(node_2, Event.CHANNEL_PENDING) diff --git a/src/builder.rs b/src/builder.rs index 1158044e47..0a2b5b2db9 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2556,6 +2556,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, + pending_funding_fee_rates: Arc::new(Mutex::new(std::collections::HashMap::new())), #[cfg(feature = "unified-payments")] hrn_resolver, prober, diff --git a/src/event.rs b/src/event.rs index 871e4656bc..47a86ba71a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -7,13 +7,13 @@ use core::future::Future; use core::task::{Poll, Waker}; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::ops::Deref; use std::sync::{Arc, Mutex}; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint}; +use bitcoin::{Amount, FeeRate, OutPoint}; use lightning::blinded_path::message::NextMessageHop; use lightning::events::bump_transaction::BumpTransactionEvent; #[cfg(not(feature = "uniffi"))] @@ -574,6 +574,7 @@ where runtime: Arc, logger: L, config: Arc, + pending_funding_fee_rates: Arc>>, } impl EventHandler @@ -590,6 +591,7 @@ where keys_manager: Arc, static_invoice_store: Option, onion_messenger: Arc, om_mailbox: Option>, prober: Option>, runtime: Arc, logger: L, config: Arc, + pending_funding_fee_rates: Arc>>, ) -> Self { Self { event_queue, @@ -611,6 +613,7 @@ where runtime, logger, config, + pending_funding_fee_rates, } } @@ -750,6 +753,10 @@ where let cur_height = self.channel_manager.current_best_block().height; let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO); + // Look up and remove any pending fee-rate override for this channel. + let fee_rate_override = + self.pending_funding_fee_rates.lock().expect("lock").remove(&user_channel_id); + // Sign the final funding transaction and broadcast it. let channel_amount = Amount::from_sat(channel_value_satoshis); let funding_transaction = self @@ -759,6 +766,7 @@ where channel_amount, confirmation_target, locktime, + fee_rate_override, ) .await; match funding_transaction { @@ -1935,6 +1943,10 @@ where } => { log_info!(self.logger, "Channel {} closed due to: {}", channel_id, reason); + // Clean up any funding fee-rate override still pending if the channel went + // away before the funding transaction was generated. + self.pending_funding_fee_rates.lock().expect("lock").remove(&user_channel_id); + // `counterparty_node_id` has been set on every `ChannelClosed` since LDK 0.0.117. let counterparty_node_id = counterparty_node_id .expect("counterparty_node_id is always set since LDK 0.0.117"); diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index 32464d0445..0c25b8c506 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -31,6 +31,14 @@ pub fn maybe_wrap(ldk_type: impl Into) -> std::sync::Arc { std::sync::Arc::new(ldk_type.into()) } +#[cfg(feature = "uniffi")] +pub(crate) type FfiFeeRate = std::sync::Arc; + +#[cfg(feature = "uniffi")] +pub(crate) fn maybe_map_fee_rate_opt(fee_rate: Option) -> Option { + fee_rate.map(|f| *f) +} + #[cfg(not(feature = "uniffi"))] pub fn maybe_deref(value: &T) -> &T { value @@ -45,3 +53,11 @@ pub fn maybe_try_convert_enum(value: &T) -> Result<&T, crate::error::Error> { pub fn maybe_wrap(value: T) -> T { value } + +#[cfg(not(feature = "uniffi"))] +pub(crate) type FfiFeeRate = bitcoin::FeeRate; + +#[cfg(not(feature = "uniffi"))] +pub(crate) fn maybe_map_fee_rate_opt(fee_rate: Option) -> Option { + fee_rate +} diff --git a/src/lib.rs b/src/lib.rs index fbb65b0005..e2b721849d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ //! //! let node_id = PublicKey::from_str("NODE_ID").unwrap(); //! let node_addr = SocketAddress::from_str("IP_ADDR:PORT").unwrap(); -//! node.open_channel(node_id, node_addr, 10000, None, None).unwrap(); +//! node.open_channel(node_id, node_addr, 10000, None, None, None).unwrap(); //! //! let event = node.wait_next_event(); //! println!("EVENT: {:?}", event); @@ -200,7 +200,7 @@ pub use types::{ pub use vss_client; use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY}; -use crate::ffi::{maybe_deref, maybe_wrap}; +use crate::ffi::{maybe_deref, maybe_map_fee_rate_opt, maybe_wrap, FfiFeeRate}; use crate::liquidity::Liquidity; use crate::scoring::setup_background_pathfinding_scores_sync; use crate::wallet::FundingAmount; @@ -280,6 +280,7 @@ pub struct Node { node_metrics: Arc, om_mailbox: Option>, async_payments_role: Option, + pending_funding_fee_rates: Arc>>, #[cfg(feature = "unified-payments")] hrn_resolver: HRNResolver, prober: Option>, @@ -700,6 +701,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), + Arc::clone(&self.pending_funding_fee_rates), )); if let Some(prober) = self.prober.clone() { @@ -1348,6 +1350,7 @@ impl Node { &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: FundingAmount, push_to_counterparty_msat: Option, channel_config: Option, announce_for_forwarding: bool, disable_counterparty_reserve: bool, + fee_rate: Option, ) -> Result { if !*self.is_running.read().expect("lock") { return Err(Error::NotRunning); @@ -1380,8 +1383,9 @@ impl Node { 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 fee_rate = fee_rate.unwrap_or_else(|| { + self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding) + }); let amount = self.wallet.get_max_funding_amount(total_anchor_reserve_sats, fee_rate)?; @@ -1420,6 +1424,12 @@ impl Node { .expect("a 16-byte slice should convert into a [u8; 16]"), ); + // Register the fee-rate override before creating the channel, as the + // `FundingGenerationReady` event may be handled before `create_channel` returns. + if let Some(fee_rate) = fee_rate { + self.pending_funding_fee_rates.lock().expect("lock").insert(user_channel_id, fee_rate); + } + let result = if disable_counterparty_reserve { self.channel_manager.create_channel_to_trusted_peer_0reserve( peer_info.node_id, @@ -1460,6 +1470,7 @@ impl Node { zero_reserve_string, e ); + self.pending_funding_fee_rates.lock().expect("lock").remove(&user_channel_id); Err(Error::ChannelCreationFailed) }, } @@ -1525,13 +1536,18 @@ impl Node { /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before /// opening the channel. /// + /// If `fee_rate` is set it will be used for the funding transaction. Otherwise we'll + /// retrieve a reasonable estimate from the configured chain source. + /// /// 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( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, - push_to_counterparty_msat: Option, channel_config: Option, + push_to_counterparty_msat: Option, fee_rate: Option, + channel_config: Option, ) -> Result { + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, address, @@ -1540,6 +1556,7 @@ impl Node { channel_config, false, false, + fee_rate_opt, ) } @@ -1561,18 +1578,23 @@ impl Node { /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before /// opening the channel. /// + /// If `fee_rate` is set it will be used for the funding transaction. Otherwise we'll + /// retrieve a reasonable estimate from the configured chain source. + /// /// 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( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, - push_to_counterparty_msat: Option, channel_config: Option, + push_to_counterparty_msat: Option, fee_rate: 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); } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, address, @@ -1581,6 +1603,7 @@ impl Node { channel_config, true, false, + fee_rate_opt, ) } @@ -1595,13 +1618,17 @@ impl Node { /// 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. /// + /// If `fee_rate` is set it will be used for the funding transaction. Otherwise we'll + /// retrieve a reasonable estimate from the configured chain source. + /// /// 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, + fee_rate: Option, channel_config: Option, ) -> Result { + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, address, @@ -1610,6 +1637,7 @@ impl Node { channel_config, false, false, + fee_rate_opt, ) } @@ -1628,18 +1656,22 @@ impl Node { /// 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. /// + /// If `fee_rate` is set it will be used for the funding transaction. Otherwise we'll + /// retrieve a reasonable estimate from the configured chain source. + /// /// 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, + fee_rate: 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); } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, address, @@ -1648,6 +1680,7 @@ impl Node { channel_config, true, false, + fee_rate_opt, ) } @@ -1667,13 +1700,18 @@ impl Node { /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before /// opening the channel. /// + /// If `fee_rate` is set it will be used for the funding transaction. Otherwise we'll + /// retrieve a reasonable estimate from the configured chain source. + /// /// 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_0reserve_channel( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, - push_to_counterparty_msat: Option, channel_config: Option, + push_to_counterparty_msat: Option, fee_rate: Option, + channel_config: Option, ) -> Result { + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, address, @@ -1682,6 +1720,7 @@ impl Node { channel_config, false, true, + fee_rate_opt, ) } @@ -1698,11 +1737,15 @@ impl Node { /// 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. /// + /// If `fee_rate` is set it will be used for the funding transaction. Otherwise we'll + /// retrieve a reasonable estimate from the configured chain source. + /// /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. pub fn open_0reserve_channel_with_all( &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, - channel_config: Option, + fee_rate: Option, channel_config: Option, ) -> Result { + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, address, @@ -1711,6 +1754,7 @@ impl Node { channel_config, false, true, + fee_rate_opt, ) } diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index ad0a2d46c7..141b2e3b23 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -14,29 +14,12 @@ use lightning::ln::channelmanager::PaymentId; use crate::config::Config; use crate::error::Error; +use crate::ffi::{maybe_map_fee_rate_opt, FfiFeeRate}; use crate::logger::{log_info, LdkLogger, Logger}; use crate::runtime::Runtime; use crate::types::{ChannelManager, Wallet}; use crate::wallet::OnchainSendAmount; -#[cfg(not(feature = "uniffi"))] -type FeeRate = bitcoin::FeeRate; -#[cfg(feature = "uniffi")] -type FeeRate = Arc; - -macro_rules! maybe_map_fee_rate_opt { - ($fee_rate_opt:expr) => {{ - #[cfg(not(feature = "uniffi"))] - { - $fee_rate_opt - } - #[cfg(feature = "uniffi")] - { - $fee_rate_opt.map(|f| *f) - } - }}; -} - /// A payment handler allowing to send and receive on-chain payments. /// /// Should be retrieved by calling [`Node::onchain_payment`]. @@ -94,9 +77,9 @@ impl OnchainPayment { /// /// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats pub fn send_to_address( - &self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option, + &self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option, ) -> Result { - let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate); + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.runtime.block_on(self.send_to_address_inner(address, amount_sats, fee_rate_opt)) } @@ -116,7 +99,7 @@ impl OnchainPayment { /// /// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::balance::BalanceDetails::spendable_onchain_balance_sats pub fn send_all_to_address( - &self, address: &bitcoin::Address, retain_reserves: bool, fee_rate: Option, + &self, address: &bitcoin::Address, retain_reserves: bool, fee_rate: Option, ) -> Result { if !*self.is_running.read().expect("lock") { return Err(Error::NotRunning); @@ -130,7 +113,7 @@ impl OnchainPayment { OnchainSendAmount::AllDrainingReserve }; - let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate); + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.runtime.block_on(self.wallet.send_to_address(address, send_amount, fee_rate_opt)) } @@ -149,11 +132,11 @@ impl OnchainPayment { /// /// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats pub fn bump_fee_rbf( - &self, payment_id: PaymentId, fee_rate: Option, + &self, payment_id: PaymentId, fee_rate: Option, ) -> Result { let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); - let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate); + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.runtime.block_on(self.wallet.bump_fee_rbf( payment_id, fee_rate_opt, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 13a8ef4e00..fd3b2bc78f 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -608,9 +608,11 @@ impl Wallet { #[allow(deprecated)] pub(crate) async fn create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, - locktime: LockTime, + locktime: LockTime, fee_rate: Option, ) -> Result { - let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target); + // Use the set fee_rate or default to fee estimation. + let fee_rate = + fee_rate.unwrap_or_else(|| self.fee_estimator.estimate_fee_rate(confirmation_target)); let mut locked_persister = self.persister.lock().await; let (psbt, change_set) = { let mut locked_wallet = self.inner.lock().expect("lock"); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 91e23ca168..11ef3b071d 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1134,6 +1134,7 @@ pub async fn open_channel_no_wait( funding_amount_sat, push_amount_msat, None, + None, ) .unwrap(); } else { @@ -1144,6 +1145,7 @@ pub async fn open_channel_no_wait( funding_amount_sat, push_amount_msat, None, + None, ) .unwrap(); } @@ -1187,6 +1189,7 @@ pub async fn open_channel_with_all( node_b.listening_addresses().unwrap().first().unwrap().clone(), None, None, + None, ) .unwrap(); } else { @@ -1196,6 +1199,7 @@ pub async fn open_channel_with_all( node_b.listening_addresses().unwrap().first().unwrap().clone(), None, None, + None, ) .unwrap(); } @@ -1284,6 +1288,7 @@ pub(crate) async fn do_channel_full_cycle( funding_amount_sat, Some(push_msat), None, + None, ) .unwrap(); } else { @@ -1294,6 +1299,7 @@ pub(crate) async fn do_channel_full_cycle( funding_amount_sat, Some(push_msat), None, + None, ) .unwrap(); } diff --git a/tests/common/scenarios/channel.rs b/tests/common/scenarios/channel.rs index 038a016ad8..10a8aaf0cc 100644 --- a/tests/common/scenarios/channel.rs +++ b/tests/common/scenarios/channel.rs @@ -25,7 +25,7 @@ pub(crate) async fn open_channel_to_external( let ext_node_id = peer.get_node_id().await.unwrap(); let ext_addr = peer.get_listening_address().await.unwrap(); - node.open_channel(ext_node_id, ext_addr, funding_amount_sat, push_msat, None).unwrap(); + node.open_channel(ext_node_id, ext_addr, funding_amount_sat, push_msat, None, None).unwrap(); let funding_txo = expect_channel_pending_event!(node, ext_node_id); super::super::wait_for_tx(electrs, funding_txo.txid).await; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 3da60800c6..5384d078cf 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -34,6 +34,7 @@ use common::{ TestNode, TestStoreType, TestSyncStore, }; use electrsd::corepc_node::{self, Node as BitcoinD}; +use electrsd::electrum_client::ElectrumApi; use electrsd::ElectrsD; use ldk_node::config::{ AsyncPaymentsRole, EsploraSyncConfig, ForwardedPaymentTrackingMode, ADDRESS_POOL_SIZE, @@ -632,6 +633,7 @@ async fn channel_open_fails_when_funds_insufficient() { 120000, None, None, + None, ) ); } @@ -980,6 +982,7 @@ async fn split_underpaid_bolt11_payment() { channel_amount_sat, push_amount_msat, None, + None, ) .unwrap(); @@ -5003,23 +5006,30 @@ fn open_channel_variant( let address = node_b.listening_addresses().unwrap().first().unwrap().clone(); match variant { OpenChannelVariant::Standard => node_a - .open_channel(node_b.node_id(), address, channel_amount_sats, None, None) + .open_channel(node_b.node_id(), address, channel_amount_sats, None, None, None) .map(|_| ()), OpenChannelVariant::Announced => node_a - .open_announced_channel(node_b.node_id(), address, channel_amount_sats, None, None) + .open_announced_channel( + node_b.node_id(), + address, + channel_amount_sats, + None, + None, + None, + ) .map(|_| ()), OpenChannelVariant::ZeroReserve => node_a - .open_0reserve_channel(node_b.node_id(), address, channel_amount_sats, None, None) + .open_0reserve_channel(node_b.node_id(), address, channel_amount_sats, None, None, None) .map(|_| ()), OpenChannelVariant::StandardWithAll => { - node_a.open_channel_with_all(node_b.node_id(), address, None, None).map(|_| ()) + node_a.open_channel_with_all(node_b.node_id(), address, None, None, None).map(|_| ()) }, OpenChannelVariant::AnnouncedWithAll => node_a - .open_announced_channel_with_all(node_b.node_id(), address, None, None) + .open_announced_channel_with_all(node_b.node_id(), address, None, None, None) + .map(|_| ()), + OpenChannelVariant::ZeroReserveWithAll => node_a + .open_0reserve_channel_with_all(node_b.node_id(), address, None, None, None) .map(|_| ()), - OpenChannelVariant::ZeroReserveWithAll => { - node_a.open_0reserve_channel_with_all(node_b.node_id(), address, None, None).map(|_| ()) - }, } } @@ -5301,3 +5311,87 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { cheap.stop().unwrap(); expensive.stop().unwrap(); } + +fn tx_fee_rate_sat_per_vb(electrs: &E, txid: Txid) -> f64 { + let tx = electrs.transaction_get(&txid).unwrap(); + let input_sats: u64 = tx + .input + .iter() + .map(|txin| { + let prev_tx = electrs.transaction_get(&txin.previous_output.txid).unwrap(); + prev_tx.output[txin.previous_output.vout as usize].value.to_sat() + }) + .sum(); + let output_sats: u64 = tx.output.iter().map(|txout| txout.value.to_sat()).sum(); + (input_sats - output_sats) as f64 / (tx.weight().to_wu() as f64 / 4.0) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn open_channel_with_fee_rate_override() { + 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); + let (node_c, node_d) = setup_two_nodes(&chain_source, false, false); + + let premine_amount_sat = 1_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![ + node_a.onchain_payment().new_address().unwrap(), + node_c.onchain_payment().new_address().unwrap(), + ], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_c.sync_wallets().unwrap(); + + // Well above the ~1 sat/vB the estimator yields on regtest. + let fee_rate_sat_per_vb = 25; + let fee_rate = bitcoin::FeeRate::from_sat_per_vb(fee_rate_sat_per_vb).unwrap(); + + // Exact amount. + node_a + .open_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + 500_000, + None, + Some(fee_rate.into()), + None, + ) + .unwrap(); + let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id()); + expect_channel_pending_event!(node_b, node_a.node_id()); + wait_for_tx(&electrsd.client, funding_txo_a.txid).await; + let actual = tx_fee_rate_sat_per_vb(&electrsd.client, funding_txo_a.txid); + assert!( + (actual - fee_rate_sat_per_vb as f64).abs() < 0.5, + "funding tx paid {actual} sat/vB, expected {fee_rate_sat_per_vb} sat/vB" + ); + + // Max amount: the amount must be computed at the overridden rate, otherwise funding fails. + node_c + .open_channel_with_all( + node_d.node_id(), + node_d.listening_addresses().unwrap().first().unwrap().clone(), + None, + Some(fee_rate.into()), + None, + ) + .unwrap(); + let funding_txo_c = expect_channel_pending_event!(node_c, node_d.node_id()); + expect_channel_pending_event!(node_d, node_c.node_id()); + wait_for_tx(&electrsd.client, funding_txo_c.txid).await; + let actual = tx_fee_rate_sat_per_vb(&electrsd.client, funding_txo_c.txid); + assert!( + (actual - fee_rate_sat_per_vb as f64).abs() < 0.5, + "funding tx paid {actual} sat/vB, expected {fee_rate_sat_per_vb} sat/vB" + ); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); + node_c.stop().unwrap(); + node_d.stop().unwrap(); +} diff --git a/tests/upgrade_downgrade_tests.rs b/tests/upgrade_downgrade_tests.rs index 593dfbb069..fe31989c7a 100644 --- a/tests/upgrade_downgrade_tests.rs +++ b/tests/upgrade_downgrade_tests.rs @@ -83,6 +83,7 @@ async fn v0_7_for_hash_payments_can_be_manually_resolved_after_upgrade() { CHANNEL_AMOUNT_SAT, None, None, + None, ) .unwrap();