Skip to content

LSPS2: Fail (or abandon) intercepted HTLCs if LSP channel open fails #3712

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
121 changes: 119 additions & 2 deletions lightning-liquidity/src/lsps2/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use alloc::string::{String, ToString};
use alloc::vec::Vec;
use lightning::util::hash_tables::HashSet;

use core::ops::Deref;
use core::sync::atomic::{AtomicUsize, Ordering};
Expand All @@ -32,12 +33,11 @@ use crate::prelude::{new_hash_map, HashMap};
use crate::sync::{Arc, Mutex, MutexGuard, RwLock};

use lightning::events::HTLCDestination;
use lightning::ln::channelmanager::{AChannelManager, InterceptId};
use lightning::ln::channelmanager::{AChannelManager, FailureCode, InterceptId};
use lightning::ln::msgs::{ErrorAction, LightningError};
use lightning::ln::types::ChannelId;
use lightning::util::errors::APIError;
use lightning::util::logger::Level;

use lightning_types::payment::PaymentHash;

use bitcoin::secp256k1::PublicKey;
Expand Down Expand Up @@ -1005,6 +1005,123 @@ where
Ok(())
}

/// Abandons a channel open attempt by pruning all state related to the channel open.
///
/// This function should be used when a channel open attempt is to be abandoned entirely,
/// without resetting the state for a potential payment retry. It removes the intercept SCID
/// mapping along with any outbound channel state and related channel ID mappings associated with
/// the specified `user_channel_id`.
pub fn channel_open_abandoned(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think to make this a bit safer, it should probably fail if we're already at PendingPaymentForward or later, i.e., we already opened the channel?

Also, what would happen if this is called after the LSPS2ServiceEvent::OpenChannel has been emitted?

Copy link
Author

Choose a reason for hiding this comment

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

Now when abandon() is called, if it's not at PendingInitialPayment or PendingChannelOpen, then it fails.

Also, what would happen if this is called after the LSPS2ServiceEvent::OpenChannel has been emitted?

You mean if the channel is already opened, but our state machine hasn’t caught up yet and still thinks we're at PendingChannelOpen? (Is that possible?). In that case, if we call abandon(), we’d end up forgetting about a channel that was actually opened, potentially leaving funds locked. If that scenario is possible and we can't reliably detect it, then this version of abandon() isn't safe.

&self, counterparty_node_id: &PublicKey, user_channel_id: u128,
) -> Result<(), APIError> {
let outer_state_lock = self.per_peer_state.read().unwrap();
let inner_state_lock =
outer_state_lock.get(counterparty_node_id).ok_or_else(|| APIError::APIMisuseError {
err: format!("No counterparty state for: {}", counterparty_node_id),
})?;
let mut peer_state = inner_state_lock.lock().unwrap();

let intercept_scid = peer_state
.intercept_scid_by_user_channel_id
.remove(&user_channel_id)
.ok_or_else(|| APIError::APIMisuseError {
err: format!("Could not find a channel with user_channel_id {}", user_channel_id),
})?;

if let Some(jit_channel) =
peer_state.outbound_channels_by_intercept_scid.get(&intercept_scid)
{
if !matches!(
jit_channel.state,
OutboundJITChannelState::PendingInitialPayment { .. }
| OutboundJITChannelState::PendingChannelOpen { .. }
) {
return Err(APIError::APIMisuseError {
err: "Cannot abandon channel open after channel creation or payment forwarding"
.to_string(),
});
}
}

peer_state.outbound_channels_by_intercept_scid.remove(&intercept_scid);

peer_state.intercept_scid_by_channel_id.retain(|_, &mut scid| scid != intercept_scid);

Ok(())
}

/// Used to fail intercepted HTLCs backwards when a channel open attempt ultimately fails.
///
/// This function should be called after receiving an [`LSPS2ServiceEvent::OpenChannel`] event
/// but only if the channel could not be successfully established. It resets the JIT channel
/// state so that the payer may try the payment again.
///
/// [`LSPS2ServiceEvent::OpenChannel`]: crate::lsps2::event::LSPS2ServiceEvent::OpenChannel
pub fn channel_open_failed(
&self, counterparty_node_id: &PublicKey, user_channel_id: u128,
) -> Result<(), APIError> {
let outer_state_lock = self.per_peer_state.read().unwrap();

let inner_state_lock =
outer_state_lock.get(counterparty_node_id).ok_or_else(|| APIError::APIMisuseError {
err: format!("No counterparty state for: {}", counterparty_node_id),
})?;

let mut peer_state = inner_state_lock.lock().unwrap();

let intercept_scid = peer_state
.intercept_scid_by_user_channel_id
.get(&user_channel_id)
.copied()
.ok_or_else(|| APIError::APIMisuseError {
err: format!("Could not find a channel with user_channel_id {}", user_channel_id),
})?;

let jit_channel = peer_state
.outbound_channels_by_intercept_scid
.get_mut(&intercept_scid)
.ok_or_else(|| APIError::APIMisuseError {
err: format!(
"Failed to map the stored intercept_scid {} for the provided user_channel_id {} to a channel.",
intercept_scid, user_channel_id,
),
})?;

if !matches!(jit_channel.state, OutboundJITChannelState::PendingChannelOpen { .. }) {
return Err(APIError::APIMisuseError {
err: "Channel is not in the PendingChannelOpen state.".to_string(),
});
}

let payment_queue_arc =
if let OutboundJITChannelState::PendingChannelOpen { payment_queue, .. } =
&jit_channel.state
{
Arc::clone(payment_queue)
} else {
unreachable!()
};
let mut queue = payment_queue_arc.lock().unwrap();
let payment_hashes: Vec<_> = queue
.clear()
.into_iter()
.map(|htlc| htlc.payment_hash)
.collect::<HashSet<_>>()
.into_iter()
.collect();
for payment_hash in payment_hashes {
self.channel_manager
.get_cm()
.fail_htlc_backwards_with_reason(&payment_hash, FailureCode::TemporaryNodeFailure);
}

jit_channel.state = OutboundJITChannelState::PendingInitialPayment {
payment_queue: Arc::clone(&payment_queue_arc),
};

Ok(())
}

/// Forward [`Event::ChannelReady`] event parameters into this function.
///
/// Will forward the intercepted HTLC if it matches a channel
Expand Down
Loading
Loading