Repository navigation
Forward channel closures and failed opens to the LSPS2 service #1233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams; | |
| use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; | ||
| use lightning_types::payment::PaymentHash; | ||
|
|
||
| use crate::logger::{log_error, LdkLogger}; | ||
| use crate::logger::{log_error, log_trace, LdkLogger}; | ||
| use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; | ||
| use crate::{total_anchor_channels_reserve_sats, Config}; | ||
|
|
||
|
|
@@ -260,6 +260,50 @@ where | |
| } | ||
| } | ||
|
|
||
| pub(crate) async fn handle_channel_closed( | ||
| &self, user_channel_id: u128, channel_id: ChannelId, counterparty_node_id: &PublicKey, | ||
| ) { | ||
| if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { | ||
| // A JIT channel closing before it became ready is a failed channel open. This errors | ||
| // for any channel that isn't such a JIT channel, which is the common case. | ||
| if let Err(e) = lsps2_service_handler | ||
| .channel_open_failed(counterparty_node_id, user_channel_id) | ||
| .await | ||
| { | ||
| log_trace!( | ||
| self.logger, | ||
| "LSPS2 service did not treat the closure of channel {} as a failed channel open: {:?}", | ||
| channel_id, | ||
| e | ||
| ); | ||
| } | ||
|
|
||
| if let Err(e) = lsps2_service_handler.channel_closed(channel_id).await { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 lightning-liquidity's |
||
| log_error!( | ||
| self.logger, | ||
| "LSPS2 service failed to handle ChannelClosed event: {:?}", | ||
| e | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Fails the HTLCs held for the given JIT channel back, allowing the payer to retry. | ||
| async fn fail_channel_open(&self, counterparty_node_id: &PublicKey, user_channel_id: u128) { | ||
| if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { | ||
| if let Err(e) = lsps2_service_handler | ||
| .channel_open_failed(counterparty_node_id, user_channel_id) | ||
| .await | ||
| { | ||
| log_error!( | ||
| self.logger, | ||
| "LSPS2 service failed to handle failed channel open: {:?}", | ||
| e | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub(crate) async fn handle_event(&self, event: LSPS2ServiceEvent) { | ||
| match event { | ||
| LSPS2ServiceEvent::GetInfo { request_id, counterparty_node_id, token } => { | ||
|
|
@@ -421,29 +465,33 @@ where | |
| return; | ||
| }; | ||
|
|
||
| let init_features = if let Some(Some(peer_manager)) = | ||
| self.peer_manager.read().expect("lock").as_ref().map(|weak| weak.upgrade()) | ||
| { | ||
| // Fail if we're not connected to the prospective channel partner. | ||
| if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { | ||
| peer.init_features | ||
| } else { | ||
| // TODO: We just silently fail here. Eventually we will need to remember | ||
| // the pending requests and regularly retry opening the channel until we | ||
| // succeed. | ||
| log_error!( | ||
| self.logger, | ||
| "Failed to open LSPS2 channel to {} due to peer not being not connected.", | ||
| their_network_key, | ||
| ); | ||
| return; | ||
| } | ||
| } else { | ||
| let peer_manager = self | ||
| .peer_manager | ||
| .read() | ||
| .expect("lock") | ||
| .as_ref() | ||
| .and_then(|weak| weak.upgrade()); | ||
| let Some(peer_manager) = peer_manager else { | ||
| debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); | ||
| log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); | ||
| return; | ||
| }; | ||
|
|
||
| // Fail if we're not connected to the prospective channel partner. | ||
| let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) else { | ||
| log_error!( | ||
| self.logger, | ||
| "Failed to open LSPS2 channel to {} due to peer not being not connected.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-existing but need to drop one "not" here. |
||
| their_network_key, | ||
| ); | ||
| // TODO: We fail the payment back here. Eventually we will need to remember | ||
| // the pending requests and regularly retry opening the channel until we | ||
| // succeed. | ||
| self.fail_channel_open(&their_network_key, user_channel_id).await; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 The insufficient-funds path is tested, but this one isn't, and it's easy to trigger. For example: stop the client, pay, expect |
||
| return; | ||
| }; | ||
| let init_features = peer.init_features; | ||
|
|
||
| // Fail if we have insufficient onchain funds available. | ||
| let over_provisioning_msat = amt_to_forward_msat | ||
| .saturating_mul(service_config.channel_over_provisioning_ppm as u64) | ||
|
|
@@ -468,9 +516,10 @@ where | |
| "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", | ||
| spendable_amount_sats, required_funds_sats, | ||
| ); | ||
| // TODO: We just silently fail here. Eventually we will need to remember | ||
| // TODO: We fail the payment back here. Eventually we will need to remember | ||
| // the pending requests and regularly retry opening the channel until we | ||
| // succeed. | ||
| self.fail_channel_open(&their_network_key, user_channel_id).await; | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -517,9 +566,6 @@ where | |
| match result { | ||
| Ok(_) => {}, | ||
| Err(e) => { | ||
| // TODO: We just silently fail here. Eventually we will need to remember | ||
| // the pending requests and regularly retry opening the channel until we | ||
| // succeed. | ||
| let zero_reserve_string = | ||
| if service_config.disable_client_reserve { "0reserve " } else { "" }; | ||
| log_error!( | ||
|
|
@@ -529,6 +575,10 @@ where | |
| their_network_key, | ||
| e | ||
| ); | ||
| // TODO: We fail the payment back here. Eventually we will need to remember | ||
| // the pending requests and regularly retry opening the channel until we | ||
| // succeed. | ||
| self.fail_channel_open(&their_network_key, user_channel_id).await; | ||
| return; | ||
| }, | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 If
add_eventfails after this, theChannelClosedis replayed and we callchannel_open_failedagain. If the payer re-sent in the meantime and a second channel is pending, the replay fails its HTLCs and resets the state. That channel'schannel_readythen errors, so we fund a channel with nothing to forward.The window is narrow, but a cheap guard would be to skip this when
list_channels_with_counterpartystill has another channel with the sameuser_channel_id.