Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@
current chain tip now aborts with a new `BuildError::ChainTipFetchFailed` variant instead of
silently pinning the wallet birthday to genesis, which would have forced a full-history rescan
once the chain source became reachable again. (#884)
- Opening a channel with an exact amount now accounts for the funding transaction's fee when
checking the anchor reserve, and fails with `NodeError::InsufficientFunds` if the amount exceeds
what the corresponding `_with_all` variant would fund at the same fee rate. Previously, a high
fee rate could spend part of the anchor reserve, or the funding transaction could fail to be
built after the channel had already been negotiated.
- When acting as an LSPS2 service, we likewise account for the funding transaction's fee when
checking that a JIT channel can be opened while retaining the anchor reserve.

# 0.7.0 - Dec. 3, 2025
This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend.
Expand Down
1 change: 1 addition & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,7 @@ fn build_with_store_internal(
Arc::clone(&channel_manager),
Arc::clone(&keys_manager),
Arc::clone(&tx_broadcaster),
Arc::clone(&fee_estimator),
Arc::clone(&kv_store),
Arc::clone(&config),
Arc::clone(&logger),
Expand Down
68 changes: 52 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1392,11 +1392,20 @@ impl Node {
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
})?;

let funding_fee_rate = fee_rate.unwrap_or_else(|| {
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding)
});

let channel_amount_sats = match channel_amount_sats {
FundingAmount::Exact { amount_sats } => {
// Check funds availability after connection (includes anchor reserve
// calculation).
self.check_sufficient_onchain_funds(amount_sats, &peer_info.node_id, true)?;
// Check funds availability after connection (includes anchor reserve and
// funding fee calculation).
self.check_sufficient_onchain_funds(
amount_sats,
&peer_info.node_id,
true,
funding_fee_rate,
)?;
amount_sats
},
FundingAmount::Max => {
Expand All @@ -1408,18 +1417,15 @@ impl Node {
let total_anchor_reserve_sats =
cur_anchor_reserve_sats.saturating_add(new_channel_reserve);

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)?;
let amount = self
.wallet
.get_max_funding_amount(total_anchor_reserve_sats, funding_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(),
funding_fee_rate.to_sat_per_kwu(),
total_anchor_reserve_sats,
);

Expand Down Expand Up @@ -1513,6 +1519,7 @@ impl Node {

fn check_sufficient_onchain_funds(
&self, amount_sats: u64, peer_node_id: &PublicKey, for_new_channel: bool,
fee_rate: bitcoin::FeeRate,
) -> Result<(), Error> {
let action_str = if for_new_channel { "create channel" } else { "splice-in" };
let cur_anchor_reserve_sats =
Expand All @@ -1532,8 +1539,8 @@ impl Node {
}

if for_new_channel {
let required_funds_sats =
amount_sats.saturating_add(self.new_channel_anchor_reserve_sats(peer_node_id)?);
let new_channel_reserve = self.new_channel_anchor_reserve_sats(peer_node_id)?;
let required_funds_sats = amount_sats.saturating_add(new_channel_reserve);

if spendable_amount_sats < required_funds_sats {
log_error!(self.logger,
Expand All @@ -1542,6 +1549,24 @@ impl Node {
);
return Err(Error::InsufficientFunds);
}

// The funding transaction's fee is paid from the same on-chain funds, so we can't
// fund more than we would when opening the channel with all of them.
let total_anchor_reserve_sats =
cur_anchor_reserve_sats.saturating_add(new_channel_reserve);
let max_amount =
self.wallet.get_max_funding_amount(total_anchor_reserve_sats, fee_rate)?;
if amount_sats > max_amount {
log_error!(
self.logger,
"Unable to create channel due to insufficient funds. Requested: {}sats, Maximum: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)",
amount_sats,
max_amount,
fee_rate.to_sat_per_kwu(),
total_anchor_reserve_sats,
);
return Err(Error::InsufficientFunds);
}
}

Ok(())
Expand All @@ -1562,7 +1587,9 @@ impl Node {
/// 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.
/// retrieve a reasonable estimate from the configured chain source. As the funding
/// transaction's fee is paid from the same on-chain funds, `channel_amount_sats` can be at most
/// what [`Node::open_channel_with_all`] would fund at that fee rate.
///
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
///
Expand Down Expand Up @@ -1608,7 +1635,9 @@ impl Node {
/// 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.
/// retrieve a reasonable estimate from the configured chain source. As the funding
/// transaction's fee is paid from the same on-chain funds, `channel_amount_sats` can be at most
/// what [`Node::open_announced_channel_with_all`] would fund at that fee rate.
///
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
///
Expand Down Expand Up @@ -1742,7 +1771,9 @@ impl Node {
/// 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.
/// retrieve a reasonable estimate from the configured chain source. As the funding
/// transaction's fee is paid from the same on-chain funds, `channel_amount_sats` can be at most
/// what [`Node::open_0reserve_channel_with_all`] would fund at that fee rate.
///
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
///
Expand Down Expand Up @@ -1822,7 +1853,12 @@ impl Node {
let max_feerate = max_funding_feerate(min_feerate);

if let FundingAmount::Exact { amount_sats } = splice_amount_sats {
self.check_sufficient_onchain_funds(amount_sats, &counterparty_node_id, false)?;
self.check_sufficient_onchain_funds(
amount_sats,
&counterparty_node_id,
false,
min_feerate,
)?;
}

let funding_template = self
Expand Down
7 changes: 6 additions & 1 deletion src/liquidity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use tokio::sync::oneshot;

use crate::builder::BuildError;
use crate::connection::ConnectionManager;
use crate::fee_estimator::OnchainFeeEstimator;
use crate::liquidity::client::lsps1::LSPS1Client;
use crate::liquidity::client::lsps2::LSPS2Client;
use crate::liquidity::service::lsps2::{LSPS2Service, LSPS2ServiceLiquiditySource};
Expand Down Expand Up @@ -251,6 +252,7 @@ where
channel_manager: Arc<ChannelManager>,
keys_manager: Arc<KeysManager>,
tx_broadcaster: Arc<Broadcaster>,
fee_estimator: Arc<OnchainFeeEstimator>,
kv_store: Arc<DynStore>,
config: Arc<Config>,
logger: L,
Expand All @@ -262,7 +264,8 @@ where
{
pub(crate) fn new(
wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>, keys_manager: Arc<KeysManager>,
tx_broadcaster: Arc<Broadcaster>, kv_store: Arc<DynStore>, config: Arc<Config>, logger: L,
tx_broadcaster: Arc<Broadcaster>, fee_estimator: Arc<OnchainFeeEstimator>,
kv_store: Arc<DynStore>, config: Arc<Config>, logger: L,
) -> Self {
let lsp_nodes = Vec::new();
let lsps2_service = None;
Expand All @@ -273,6 +276,7 @@ where
channel_manager,
keys_manager,
tx_broadcaster,
fee_estimator,
kv_store,
config,
logger,
Expand Down Expand Up @@ -367,6 +371,7 @@ where
lsps2_service: Arc::new(LSPS2ServiceLiquiditySource {
lsps2_service: self.lsps2_service,
wallet: self.wallet,
fee_estimator: self.fee_estimator,
channel_manager: self.channel_manager,
peer_manager: RwLock::new(None),
keys_manager: self.keys_manager,
Expand Down
20 changes: 20 additions & 0 deletions src/liquidity/service/lsps2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams;
use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig;
use lightning_types::payment::PaymentHash;

use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator};
use crate::logger::{log_error, LdkLogger};
use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet};
use crate::{total_anchor_channels_reserve_sats, Config};
Expand All @@ -40,6 +41,7 @@ where
{
pub(crate) lsps2_service: Option<LSPS2Service>,
pub(crate) wallet: Arc<Wallet>,
pub(crate) fee_estimator: Arc<OnchainFeeEstimator>,
pub(crate) channel_manager: Arc<ChannelManager>,
pub(crate) peer_manager: RwLock<Option<Weak<PeerManager>>>,
pub(crate) keys_manager: Arc<KeysManager>,
Expand Down Expand Up @@ -474,6 +476,24 @@ where
return;
}

// The funding transaction's fee is paid from the same on-chain funds, so we can't
// fund more than we would when opening a channel with all of them.
let total_anchor_reserve_sats =
cur_anchor_reserve_sats.saturating_add(additional_reserve_required);
let funding_fee_rate =
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
let max_amount = self
.wallet
.get_max_funding_amount(total_anchor_reserve_sats, funding_fee_rate)
.unwrap_or(0);
if channel_amount_sats > max_amount {
log_error!(self.logger,
"Unable to create channel due to insufficient funds. Requested: {}sats, Maximum: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)",
channel_amount_sats, max_amount, funding_fee_rate.to_sat_per_kwu(), total_anchor_reserve_sats,
);
return;
}

let mut config = self.channel_manager.get_current_config().clone();

// If we act as an LSPS2 service, the HTLC-value-in-flight must be 100% of the
Expand Down
Loading
Loading