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
4 changes: 4 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ 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_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);
Expand Down
141 changes: 121 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,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: Option<u64>,
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
announce_for_forwarding: bool,
) -> Result<UserChannelId, Error> {
Expand All @@ -1111,8 +1111,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 {
Some(amount) => {
// Check funds availability after connection (includes anchor reserve
// calculation).
self.check_sufficient_funds_for_channel(amount, &peer_info.node_id)?;
amount
},
None => {
// 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;
Expand Down Expand Up @@ -1153,6 +1183,25 @@ impl Node {
}
}

fn new_channel_anchor_reserve_sats(&self, peer_node_id: &PublicKey) -> Result<u64, Error> {
let init_features = self
.peer_manager
.peer_by_node_id(peer_node_id)
.ok_or(Error::ConnectionFailed)?
.init_features;
let 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
}
});

Ok(sats)
}

fn check_sufficient_funds_for_channel(
&self, amount_sats: u64, peer_node_id: &PublicKey,
) -> Result<(), Error> {
Expand All @@ -1171,21 +1220,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,
Expand Down Expand Up @@ -1222,7 +1258,7 @@ impl Node {
self.open_channel_inner(
node_id,
address,
channel_amount_sats,
Some(channel_amount_sats),
push_to_counterparty_msat,
channel_config,
false,
Expand Down Expand Up @@ -1262,7 +1298,72 @@ impl Node {
self.open_channel_inner(
node_id,
address,
channel_amount_sats,
Some(channel_amount_sats),
push_to_counterparty_msat,
channel_config,
true,
)
}

/// Connect to a node and open a new unannounced channel, using all available on-chain funds
/// minus fees and anchor reserves.
///
/// To open an announced channel, see [`Node::open_announced_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_channel_with_all(
&self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option<u64>,
channel_config: Option<ChannelConfig>,
) -> Result<UserChannelId, Error> {
self.open_channel_inner(
node_id,
address,
None,
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<u64>,
channel_config: Option<ChannelConfig>,
) -> Result<UserChannelId, Error> {
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,
None,
push_to_counterparty_msat,
channel_config,
true,
Expand Down
84 changes: 83 additions & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub(crate) enum OnchainSendAmount {
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<PersistedWallet<KVStoreWalletPersister>>,
Expand Down Expand Up @@ -459,6 +461,87 @@ impl Wallet {
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
}

/// Returns the maximum amount available for funding a channel, accounting for on-chain fees
/// and anchor reserves.
///
/// Uses a two-pass approach: first builds a temporary transaction to estimate fees, then
/// returns the spendable balance minus those fees.
pub(crate) fn get_max_funding_amount(
&self, cur_anchor_reserve_sats: u64, fee_rate: FeeRate,
) -> Result<u64, Error> {
let mut locked_wallet = self.inner.lock().unwrap();
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 funding amount: no spendable funds available."
);
return Err(Error::InsufficientFunds);
}

// 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 tmp_tx = if cur_anchor_reserve_sats > DUST_LIMIT_SATS {
let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0);
let mut tmp_tx_builder = locked_wallet.build_tx();
tmp_tx_builder
.drain_wallet()
.drain_to(dummy_p2wsh_script)
.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 for fee estimation: {err}"
);
return Err(err.into());
},
}
} else {
let mut tmp_tx_builder = locked_wallet.build_tx();
tmp_tx_builder.drain_wallet().drain_to(dummy_p2wsh_script).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 for fee estimation: {err}"
);
return Err(err.into());
},
}
};

let estimated_tx_fee = locked_wallet.calculate_fee(&tmp_tx).map_err(|e| {
log_error!(self.logger, "Failed to calculate fee of temporary transaction: {e}");
e
})?;

// Cancel the temporary transaction to free up any used change addresses.
locked_wallet.cancel_tx(&tmp_tx);

let max_amount = spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat());

if max_amount < DUST_LIMIT_SATS {
log_error!(
self.logger,
"Unable to open channel: available funds would be consumed entirely by fees. Available: {spendable_amount_sats}sats, estimated fee: {estimated_tx_fee}sats.",
);
return Err(Error::InsufficientFunds);
}

Ok(max_amount)
}

pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
.map_err(|_| Error::InvalidAddress)?
Expand All @@ -482,7 +565,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();
Expand Down
32 changes: 32 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,38 @@ 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(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool,
expect_anchor_channel: bool, force_close: bool,
Expand Down
Loading
Loading