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
9 changes: 5 additions & 4 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ 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, 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, 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, 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, ChannelConfig? channel_config);
[Throws=NodeError]
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
[Throws=NodeError]
Expand Down Expand Up @@ -169,6 +169,7 @@ enum NodeError {
"NotRunning",
"OnchainTxCreationFailed",
"ConnectionFailed",
"NotConnected",
"InvoiceCreationFailed",
"InvoiceRequestCreationFailed",
"OfferCreationFailed",
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub enum Error {
OnchainTxCreationFailed,
/// A network connection has been closed.
ConnectionFailed,
/// The peer is not connected.
NotConnected,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this after some time, I have to say I still find the new API changes here not great. That we suddenly have this new error case instead of just .. connecting to the peer IMO shows that it just makes the API less explicit, less clear, and more complicated, as users now suddenly will have to handle one more edge case. I'm still not entirely convinced it's worth it tbh.

However, if you insist, this error variant should like be named more along the lines of PeerAddressUnknown or similar, as the issue here really is not that we're not connected but that we now have this API that allows users to call it without all necessary data to connect to the involved peers in certain cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further thought, I'd be happy to drop the proposed API changes here too.

instead of just .. connecting to the peer

I have a question: what should we do if the user sets an onion v3 address in open_channel for the peer, but we are already connected to that peer on IPv4 ?

My initial hunch is to disconnect the peer, and reconnect over onionV3, with perhaps a warning in the logs "hey note we were already connected to that peer on IPv4".

On master, we currently silently drop the onion V3 address, and use the existing IPv4 connection, which I don't think is what we want.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial hunch is to disconnect the peer, and reconnect over onionV3, with perhaps a warning in the logs "hey note we were already connected to that peer on IPv4".

Problem with this is that if we fail to connect over tor, then we've disconnected the peer and interrupted our existing channel for nothing. We can't necessarily just reconnect to them as well as it may be an inbound connection.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks that's a good point. i think i'm good to not touch anything then.

Copy link
Copy Markdown
Contributor Author

@benthecarman benthecarman Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way to have the least surprising api would probably be to remove the socket address from open_channel completely and require the user to call connect_peer before. However, that is pretty annoying to the user and not what people have come to expect.

This PR is becoming more effort than it is worth. Fine with closing we can't come to agreement

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to close if @tnull is too

/// Invoice creation failed.
InvoiceCreationFailed,
/// Invoice request creation failed.
Expand Down Expand Up @@ -148,6 +150,7 @@ impl fmt::Display for Error {
write!(f, "On-chain transaction could not be created.")
},
Self::ConnectionFailed => write!(f, "Network connection closed."),
Self::NotConnected => write!(f, "The peer is not connected."),
Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."),
Self::InvoiceRequestCreationFailed => write!(f, "Failed to create invoice request."),
Self::OfferCreationFailed => write!(f, "Failed to create offer."),
Expand Down
103 changes: 64 additions & 39 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,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, Some(node_addr), 10000, None, None).unwrap();
//!
//! let event = node.wait_next_event();
//! println!("EVENT: {:?}", event);
Expand Down Expand Up @@ -1126,39 +1126,47 @@ impl Node {
}

fn open_channel_inner(
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: FundingAmount,
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
announce_for_forwarding: bool,
&self, node_id: PublicKey, address: Option<SocketAddress>,
channel_amount_sats: FundingAmount, push_to_counterparty_msat: Option<u64>,
channel_config: Option<ChannelConfig>, announce_for_forwarding: bool,
) -> Result<UserChannelId, Error> {
if !*self.is_running.read().unwrap() {
return Err(Error::NotRunning);
}

let peer_info = PeerInfo { node_id, address };

let con_node_id = peer_info.node_id;
let con_addr = peer_info.address.clone();
let con_cm = Arc::clone(&self.connection_manager);

// We need to use our main runtime here as a local runtime might not be around to poll
// connection futures going forward.
self.runtime.block_on(async move {
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
})?;
// if we don't have the socket address, check if we are already connected
let address = match address {
Some(address) => {
// We need to use our main runtime here as a local runtime might not be around to poll
// connection futures going forward.
let con_cm = Arc::clone(&self.connection_manager);
let con_addr = address.clone();
self.runtime.block_on(async move {
con_cm.connect_peer_if_necessary(node_id, con_addr).await
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already present, but I would find it surprising if I specified an IPV4 address, but I was already connected to the peer via OnionV3, and the IPV4 setting at channel open was silently ignored and replaced with the OnionV3 address. Works also the other way around. WDYT ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree, not really sure what the alternative would be? We could throw an error if the socket addr doesn't match the one we are currently connected with but that seems kinda flaky and not what people would expect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one potential non-flaky route in case of a mismatch: disconnect from the peer, and reconnect using the address specified in the open parameter ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though this is likely out-of-scope for this PR as we'd want things to be consistent also with the Node::connect method, feel free to dismiss this here.

Curious what @tnull thinks nonetheless.

})?;
Some(address)
},
None => {
// If we are connected, grab the socket address as we need to make sure we have it persisted
// in our peer storage for future reconnections.
let peer =
self.peer_manager.peer_by_node_id(&node_id).ok_or(Error::NotConnected)?;
peer.socket_address
},
};

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)?;
self.check_sufficient_funds_for_channel(amount_sats, &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 new_channel_reserve = self.new_channel_anchor_reserve_sats(&node_id)?;
let total_anchor_reserve_sats = cur_anchor_reserve_sats + new_channel_reserve;

let fee_rate =
Expand Down Expand Up @@ -1197,20 +1205,21 @@ impl Node {
);

match self.channel_manager.create_channel(
peer_info.node_id,
node_id,
channel_amount_sats,
push_msat,
user_channel_id,
None,
Some(user_config),
) {
Ok(_) => {
log_info!(
self.logger,
"Initiated channel creation with peer {}. ",
peer_info.node_id
);
self.peer_store.add_peer(peer_info)?;
log_info!(self.logger, "Initiated channel creation with peer {}. ", node_id);

if let Some(address) = address {
let peer_info = PeerInfo { node_id, address };
self.peer_store.add_peer(peer_info)?;
}

Ok(UserChannelId(user_channel_id))
},
Err(e) => {
Expand All @@ -1224,7 +1233,7 @@ impl Node {
let init_features = self
.peer_manager
.peer_by_node_id(peer_node_id)
.ok_or(Error::ConnectionFailed)?
.ok_or(Error::NotConnected)?
.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))
Expand Down Expand Up @@ -1262,12 +1271,16 @@ impl Node {
Ok(())
}

/// Connect to a node and open a new unannounced channel.
/// Open a new unannounced channel with a node.
///
/// To open an announced channel, see [`Node::open_announced_channel`].
///
/// Disconnects and reconnects are handled automatically.
///
/// If `address` is provided, the node will connect to the peer at the given address before
/// opening the channel. If `address` is `None`, the node must already be connected to the
/// peer, otherwise [`Error::NotConnected`] will be returned.
///
/// 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.
Expand All @@ -1280,7 +1293,7 @@ impl Node {
///
/// [`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,
&self, node_id: PublicKey, address: Option<SocketAddress>, channel_amount_sats: u64,
Comment thread
benthecarman marked this conversation as resolved.
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
) -> Result<UserChannelId, Error> {
self.open_channel_inner(
Expand All @@ -1293,16 +1306,20 @@ impl Node {
)
}

/// Connect to a node and open a new announced channel.
/// Open a new announced channel with a node.
///
/// 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 publip network graph, i.e.,
/// 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`].
///
/// Disconnects and reconnects are handled automatically.
///
/// If `address` is provided, the node will connect to the peer at the given address before
/// opening the channel. If `address` is `None`, the node must already be connected to the
/// peer, otherwise [`Error::NotConnected`] will be returned.
///
/// 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.
Expand All @@ -1315,7 +1332,7 @@ impl Node {
///
/// [`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,
&self, node_id: PublicKey, address: Option<SocketAddress>, channel_amount_sats: u64,
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
) -> Result<UserChannelId, Error> {
if let Err(err) = may_announce_channel(&self.config) {
Expand All @@ -1333,13 +1350,17 @@ impl Node {
)
}

/// Connect to a node and open a new unannounced channel, using all available on-chain funds
/// minus fees and anchor reserves.
/// Open a new unannounced channel with a node, 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 `address` is provided, the node will connect to the peer at the given address before
/// opening the channel. If `address` is `None`, the node must already be connected to the
/// peer, otherwise [`Error::NotConnected`] will be returned.
///
/// 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.
Expand All @@ -1348,8 +1369,8 @@ impl Node {
///
/// [`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>,
&self, node_id: PublicKey, address: Option<SocketAddress>,
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
) -> Result<UserChannelId, Error> {
self.open_channel_inner(
node_id,
Expand All @@ -1361,8 +1382,8 @@ impl Node {
)
}

/// Connect to a node and open a new announced channel, using all available on-chain funds
/// minus fees and anchor reserves.
/// Open a new announced channel with a node, 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.,
Expand All @@ -1372,6 +1393,10 @@ impl Node {
///
/// Disconnects and reconnects are handled automatically.
///
/// If `address` is provided, the node will connect to the peer at the given address before
/// opening the channel. If `address` is `None`, the node must already be connected to the
/// peer, otherwise [`Error::NotConnected`] will be returned.
///
/// 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.
Expand All @@ -1380,8 +1405,8 @@ impl Node {
///
/// [`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>,
&self, node_id: PublicKey, address: Option<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}");
Expand Down
10 changes: 5 additions & 5 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ pub async fn open_channel_push_amt(
node_a
.open_announced_channel(
node_b.node_id(),
node_b.listening_addresses().unwrap().first().unwrap().clone(),
node_b.listening_addresses().unwrap().first().cloned(),
funding_amount_sat,
push_amount_msat,
None,
Expand All @@ -731,7 +731,7 @@ pub async fn open_channel_push_amt(
node_a
.open_channel(
node_b.node_id(),
node_b.listening_addresses().unwrap().first().unwrap().clone(),
node_b.listening_addresses().unwrap().first().cloned(),
funding_amount_sat,
push_amount_msat,
None,
Expand All @@ -755,7 +755,7 @@ pub async fn open_channel_with_all(
node_a
.open_announced_channel_with_all(
node_b.node_id(),
node_b.listening_addresses().unwrap().first().unwrap().clone(),
node_b.listening_addresses().unwrap().first().cloned(),
None,
None,
)
Expand All @@ -764,7 +764,7 @@ pub async fn open_channel_with_all(
node_a
.open_channel_with_all(
node_b.node_id(),
node_b.listening_addresses().unwrap().first().unwrap().clone(),
node_b.listening_addresses().unwrap().first().cloned(),
None,
None,
)
Expand Down Expand Up @@ -851,7 +851,7 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
node_a
.open_announced_channel(
node_b.node_id(),
node_b.listening_addresses().unwrap().first().unwrap().clone(),
node_b.listening_addresses().unwrap().first().cloned(),
funding_amount_sat,
Some(push_msat),
None,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests_cln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async fn test_cln() {
// Open the channel
let funding_amount_sat = 1_000_000;

node.open_channel(cln_node_id, cln_address, funding_amount_sat, Some(500_000_000), None)
node.open_channel(cln_node_id, Some(cln_address), funding_amount_sat, Some(500_000_000), None)
.unwrap();

let funding_txo = common::expect_channel_pending_event!(node, cln_node_id);
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests_lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async fn test_lnd() {
// Open the channel
let funding_amount_sat = 1_000_000;

node.open_channel(lnd_node_id, lnd_address, funding_amount_sat, Some(500_000_000), None)
node.open_channel(lnd_node_id, Some(lnd_address), funding_amount_sat, Some(500_000_000), None)
.unwrap();

let funding_txo = common::expect_channel_pending_event!(node, lnd_node_id);
Expand Down
Loading
Loading