diff --git a/src/lsps2/client.rs b/src/lsps2/client.rs index d0729c4..ac79f99 100644 --- a/src/lsps2/client.rs +++ b/src/lsps2/client.rs @@ -13,7 +13,7 @@ use crate::events::{Event, EventQueue}; use crate::lsps0::msgs::{ProtocolMessageHandler, RequestId, ResponseError}; use crate::lsps2::event::LSPS2ClientEvent; use crate::message_queue::MessageQueue; -use crate::prelude::{HashMap, String, ToString}; +use crate::prelude::{HashMap, HashSet, String}; use crate::sync::{Arc, Mutex, RwLock}; use lightning::ln::msgs::{ErrorAction, LightningError}; @@ -23,6 +23,7 @@ use lightning::util::logger::Level; use bitcoin::secp256k1::PublicKey; +use core::default::Default; use core::ops::Deref; use crate::lsps2::msgs::{ @@ -32,7 +33,21 @@ use crate::lsps2::msgs::{ /// Client-side configuration options for JIT channels. #[derive(Clone, Debug, Copy)] -pub struct LSPS2ClientConfig {} +pub struct LSPS2ClientConfig { + /// Trust the LSP to create a valid channel funding transaction and have it confirmed on-chain. + /// + /// TODO: If set to `false`, we'll only release the pre-image after we see an on-chain + /// confirmation of the channel's funding transaction. + /// + /// Defaults to `true`. + pub client_trusts_lsp: bool, +} + +impl Default for LSPS2ClientConfig { + fn default() -> Self { + Self { client_trusts_lsp: true } + } +} struct ChannelStateError(String); @@ -42,44 +57,13 @@ impl From<ChannelStateError> for LightningError { } } -struct InboundJITChannelConfig { - pub user_id: u128, - pub payment_size_msat: Option<u64>, -} - #[derive(PartialEq, Debug)] enum InboundJITChannelState { - MenuRequested, - PendingMenuSelection, BuyRequested, PendingPayment { client_trusts_lsp: bool, intercept_scid: InterceptScid }, } impl InboundJITChannelState { - fn info_received(&self) -> Result<Self, ChannelStateError> { - match self { - InboundJITChannelState::MenuRequested => { - Ok(InboundJITChannelState::PendingMenuSelection) - } - state => Err(ChannelStateError(format!( - "Received unexpected get_info response. JIT Channel was in state: {:?}", - state - ))), - } - } - - fn opening_fee_params_selected(&self) -> Result<Self, ChannelStateError> { - match self { - InboundJITChannelState::PendingMenuSelection => { - Ok(InboundJITChannelState::BuyRequested) - } - state => Err(ChannelStateError(format!( - "Opening fee params selected when JIT Channel was in state: {:?}", - state - ))), - } - } - fn invoice_params_received( &self, client_trusts_lsp: bool, intercept_scid: InterceptScid, ) -> Result<Self, ChannelStateError> { @@ -97,32 +81,13 @@ impl InboundJITChannelState { struct InboundJITChannel { state: InboundJITChannelState, - config: InboundJITChannelConfig, + user_channel_id: u128, + payment_size_msat: Option<u64>, } impl InboundJITChannel { - fn new(user_id: u128, payment_size_msat: Option<u64>) -> Self { - Self { - config: InboundJITChannelConfig { user_id, payment_size_msat }, - state: InboundJITChannelState::MenuRequested, - } - } - - fn info_received(&mut self) -> Result<(), LightningError> { - self.state = self.state.info_received()?; - Ok(()) - } - - fn opening_fee_params_selected(&mut self) -> Result<(), LightningError> { - self.state = self.state.opening_fee_params_selected()?; - - match self.state { - InboundJITChannelState::BuyRequested => Ok(()), - _ => Err(LightningError { - action: ErrorAction::IgnoreAndLog(Level::Error), - err: "impossible state transition".to_string(), - }), - } + fn new(user_channel_id: u128, payment_size_msat: Option<u64>) -> Self { + Self { user_channel_id, payment_size_msat, state: InboundJITChannelState::BuyRequested } } fn invoice_params_received( @@ -135,26 +100,16 @@ impl InboundJITChannel { struct PeerState { inbound_channels_by_id: HashMap<u128, InboundJITChannel>, - request_to_cid: HashMap<RequestId, u128>, + pending_get_info_requests: HashSet<RequestId>, + pending_buy_requests: HashMap<RequestId, u128>, } impl PeerState { fn new() -> Self { let inbound_channels_by_id = HashMap::new(); - let request_to_cid = HashMap::new(); - Self { inbound_channels_by_id, request_to_cid } - } - - fn insert_inbound_channel(&mut self, jit_channel_id: u128, channel: InboundJITChannel) { - self.inbound_channels_by_id.insert(jit_channel_id, channel); - } - - fn insert_request(&mut self, request_id: RequestId, jit_channel_id: u128) { - self.request_to_cid.insert(request_id, jit_channel_id); - } - - fn remove_inbound_channel(&mut self, jit_channel_id: u128) { - self.inbound_channels_by_id.remove(&jit_channel_id); + let pending_get_info_requests = HashSet::new(); + let pending_buy_requests = HashMap::new(); + Self { inbound_channels_by_id, pending_get_info_requests, pending_buy_requests } } } @@ -167,7 +122,7 @@ where pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>, per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>, - _config: LSPS2ClientConfig, + config: LSPS2ClientConfig, } impl<ES: Deref> LSPS2ClientHandler<ES> @@ -184,38 +139,34 @@ where pending_messages, pending_events, per_peer_state: RwLock::new(HashMap::new()), - _config: config, + config, } } - /// Initiate the creation of an invoice that when paid will open a channel - /// with enough inbound liquidity to be able to receive the payment. + /// Request the channel opening parameters from the LSP. /// - /// `counterparty_node_id` is the node_id of the LSP you would like to use. + /// This initiates the JIT-channel flow that, at the end of it, will have the LSP + /// open a channel with sufficient inbound liquidity to be able to receive the payment. /// - /// If `payment_size_msat` is [`Option::Some`] then the invoice will be for a fixed amount - /// and MPP can be used to pay it. + /// The user will receive the LSP's response via an [`OpeningParametersReady`] event. /// - /// If `payment_size_msat` is [`Option::None`] then the invoice can be for an arbitrary amount - /// but MPP can no longer be used to pay it. + /// `counterparty_node_id` is the `node_id` of the LSP you would like to use. /// - /// `token` is an optional String that will be provided to the LSP. + /// `token` is an optional `String` that will be provided to the LSP. /// It can be used by the LSP as an API key, coupon code, or some other way to identify a user. - pub fn create_invoice( - &self, counterparty_node_id: PublicKey, payment_size_msat: Option<u64>, - token: Option<String>, user_channel_id: u128, - ) { - let jit_channel_id = self.generate_jit_channel_id(); - let channel = InboundJITChannel::new(user_channel_id, payment_size_msat); - - let mut outer_state_lock = self.per_peer_state.write().unwrap(); - let inner_state_lock = - outer_state_lock.entry(counterparty_node_id).or_insert(Mutex::new(PeerState::new())); - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock.insert_inbound_channel(jit_channel_id, channel); - + /// + /// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady + pub fn request_opening_params(&self, counterparty_node_id: PublicKey, token: Option<String>) { let request_id = crate::utils::generate_request_id(&self.entropy_source); - peer_state_lock.insert_request(request_id.clone(), jit_channel_id); + + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(counterparty_node_id) + .or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.pending_get_info_requests.insert(request_id.clone()); + } self.pending_messages.enqueue( &counterparty_node_id, @@ -224,65 +175,58 @@ where ); } - /// Used by client to confirm which channel parameters to use for the JIT Channel buy request. + /// Confirms a set of chosen channel opening parameters to use for the JIT channel and + /// requests the necessary invoice generation parameters from the LSP. + /// + /// Should be called in response to receiving a [`OpeningParametersReady`] event. + /// + /// The user will receive the LSP's response via an [`InvoiceParametersReady`] event. + /// + /// The user needs to provide a locally unique `user_channel_id` which will be used for + /// tracking the channel state. + /// + /// If `payment_size_msat` is [`Option::Some`] then the invoice will be for a fixed amount + /// and MPP can be used to pay it. + /// + /// If `payment_size_msat` is [`Option::None`] then the invoice can be for an arbitrary amount + /// but MPP can no longer be used to pay it. + /// /// The client agrees to paying an opening fee equal to /// `max(min_fee_msat, proportional*(payment_size_msat/1_000_000))`. /// - /// Should be called in response to receiving a [`LSPS2ClientEvent::GetInfoResponse`] event. - /// - /// [`LSPS2ClientEvent::GetInfoResponse`]: crate::lsps2::event::LSPS2ClientEvent::GetInfoResponse - pub fn opening_fee_params_selected( - &self, counterparty_node_id: PublicKey, jit_channel_id: u128, - opening_fee_params: OpeningFeeParams, + /// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady + /// [`InvoiceParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::InvoiceParametersReady + pub fn select_opening_params( + &self, counterparty_node_id: PublicKey, user_channel_id: u128, + payment_size_msat: Option<u64>, opening_fee_params: OpeningFeeParams, ) -> Result<(), APIError> { - let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { - Some(inner_state_lock) => { - let mut peer_state = inner_state_lock.lock().unwrap(); - if let Some(jit_channel) = - peer_state.inbound_channels_by_id.get_mut(&jit_channel_id) - { - match jit_channel.opening_fee_params_selected() { - Ok(()) => (), - Err(e) => { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(APIError::APIMisuseError { err: e.err }); - } - }; - - let request_id = crate::utils::generate_request_id(&self.entropy_source); - let payment_size_msat = jit_channel.config.payment_size_msat; - peer_state.insert_request(request_id.clone(), jit_channel_id); - - self.pending_messages.enqueue( - &counterparty_node_id, - LSPS2Message::Request( - request_id, - LSPS2Request::Buy(BuyRequest { opening_fee_params, payment_size_msat }), - ) - .into(), - ); - } else { - return Err(APIError::APIMisuseError { - err: format!("Channel with id {} not found", jit_channel_id), - }); - } - } - None => { - return Err(APIError::APIMisuseError { - err: format!("No existing state with counterparty {}", counterparty_node_id), - }) - } + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = + outer_state_lock.entry(counterparty_node_id).or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + let jit_channel = InboundJITChannel::new(user_channel_id, payment_size_msat); + if peer_state_lock.inbound_channels_by_id.insert(user_channel_id, jit_channel).is_some() { + return Err(APIError::APIMisuseError { + err: format!( + "Failed due to duplicate user_channel_id. Please ensure its uniqueness!" + ), + }); } - Ok(()) - } + let request_id = crate::utils::generate_request_id(&self.entropy_source); + peer_state_lock.pending_buy_requests.insert(request_id.clone(), user_channel_id); + + self.pending_messages.enqueue( + &counterparty_node_id, + LSPS2Message::Request( + request_id, + LSPS2Request::Buy(BuyRequest { opening_fee_params, payment_size_msat }), + ) + .into(), + ); - fn generate_jit_channel_id(&self) -> u128 { - let bytes = self.entropy_source.get_secure_random_bytes(); - let mut id_bytes: [u8; 16] = [0; 16]; - id_bytes.copy_from_slice(&bytes[0..16]); - u128::from_be_bytes(id_bytes) + Ok(()) } fn handle_get_info_response( @@ -293,39 +237,22 @@ where Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + if !peer_state.pending_get_info_requests.remove(&request_id) { + return Err(LightningError { err: format!( "Received get_info response for an unknown request: {:?}", request_id ), action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - let jit_channel = peer_state - .inbound_channels_by_id - .get_mut(&jit_channel_id) - .ok_or(LightningError { - err: format!( - "Received get_info response for an unknown channel: {:?}", - jit_channel_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - if let Err(e) = jit_channel.info_received() { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(e); + }); } self.pending_events.enqueue(Event::LSPS2Client( - LSPS2ClientEvent::GetInfoResponse { + LSPS2ClientEvent::OpeningParametersReady { counterparty_node_id: *counterparty_node_id, opening_fee_params_menu: result.opening_fee_params_menu, min_payment_size_msat: result.min_payment_size_msat, max_payment_size_msat: result.max_payment_size_msat, - jit_channel_id, - user_channel_id: jit_channel.config.user_id, }, )); } @@ -351,24 +278,16 @@ where Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + if !peer_state.pending_get_info_requests.remove(&request_id) { + return Err(LightningError { err: format!( "Received get_info error for an unknown request: {:?}", request_id ), action: ErrorAction::IgnoreAndLog(Level::Info), - })?; + }); + } - peer_state.inbound_channels_by_id.remove(&jit_channel_id).ok_or( - LightningError { - err: format!( - "Received get_info error for an unknown channel: {:?}", - jit_channel_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }, - )?; Ok(()) } None => { @@ -385,8 +304,8 @@ where Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + let user_channel_id = + peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError { err: format!( "Received buy response for an unknown request: {:?}", request_id @@ -396,32 +315,43 @@ where let jit_channel = peer_state .inbound_channels_by_id - .get_mut(&jit_channel_id) + .get_mut(&user_channel_id) .ok_or(LightningError { err: format!( "Received buy response for an unknown channel: {:?}", - jit_channel_id + user_channel_id ), action: ErrorAction::IgnoreAndLog(Level::Info), })?; + // Reject the buy response if we disallow client_trusts_lsp and the LSP requires + // it. + if !self.config.client_trusts_lsp && result.client_trusts_lsp { + peer_state.inbound_channels_by_id.remove(&user_channel_id); + return Err(LightningError { + err: format!( + "Aborting JIT channel flow as the LSP requires 'client_trusts_lsp' mode, which we disallow" + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + if let Err(e) = jit_channel.invoice_params_received( result.client_trusts_lsp, result.intercept_scid.clone(), ) { - peer_state.remove_inbound_channel(jit_channel_id); + peer_state.inbound_channels_by_id.remove(&user_channel_id); return Err(e); } if let Ok(intercept_scid) = result.intercept_scid.to_scid() { self.pending_events.enqueue(Event::LSPS2Client( - LSPS2ClientEvent::InvoiceGenerationReady { + LSPS2ClientEvent::InvoiceParametersReady { counterparty_node_id: *counterparty_node_id, intercept_scid, cltv_expiry_delta: result.lsp_cltv_expiry_delta, - payment_size_msat: jit_channel.config.payment_size_msat, - client_trusts_lsp: result.client_trusts_lsp, - user_channel_id: jit_channel.config.user_id, + payment_size_msat: jit_channel.payment_size_msat, + user_channel_id: jit_channel.user_channel_id, }, )); } else { @@ -455,22 +385,21 @@ where Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + let user_channel_id = + peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError { err: format!("Received buy error for an unknown request: {:?}", request_id), action: ErrorAction::IgnoreAndLog(Level::Info), })?; - let _jit_channel = peer_state - .inbound_channels_by_id - .remove(&jit_channel_id) - .ok_or(LightningError { + peer_state.inbound_channels_by_id.remove(&user_channel_id).ok_or( + LightningError { err: format!( "Received buy error for an unknown channel: {:?}", - jit_channel_id + user_channel_id ), action: ErrorAction::IgnoreAndLog(Level::Info), - })?; + }, + )?; Ok(()) } None => { diff --git a/src/lsps2/event.rs b/src/lsps2/event.rs index 101d80e..1d1922b 100644 --- a/src/lsps2/event.rs +++ b/src/lsps2/event.rs @@ -20,17 +20,11 @@ use bitcoin::secp256k1::PublicKey; pub enum LSPS2ClientEvent { /// Information from the LSP about their current fee rates and channel parameters. /// - /// You must call [`LSPS2ClientHandler::opening_fee_params_selected`] with the fee parameter + /// You must call [`LSPS2ClientHandler::select_opening_params`] with the fee parameter /// you want to use if you wish to proceed opening a channel. /// - /// [`LSPS2ClientHandler::opening_fee_params_selected`]: crate::lsps2::client::LSPS2ClientHandler::opening_fee_params_selected - GetInfoResponse { - /// This is a randomly generated identifier used to track the JIT channel state. - /// It is not related in anyway to the eventual lightning channel id. - /// It needs to be passed to [`LSPS2ClientHandler::opening_fee_params_selected`]. - /// - /// [`LSPS2ClientHandler::opening_fee_params_selected`]: crate::lsps2::client::LSPS2ClientHandler::opening_fee_params_selected - jit_channel_id: u128, + /// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params + OpeningParametersReady { /// The node id of the LSP that provided this response. counterparty_node_id: PublicKey, /// The menu of fee parameters the LSP is offering at this time. @@ -40,16 +34,20 @@ pub enum LSPS2ClientEvent { min_payment_size_msat: u64, /// The max payment size allowed when opening the channel. max_payment_size_msat: u64, - /// The user_channel_id value passed in to [`LSPS2ClientHandler::create_invoice`]. - /// - /// [`LSPS2ClientHandler::create_invoice`]: crate::lsps2::client::LSPS2ClientHandler::create_invoice - user_channel_id: u128, }, - /// Use the provided fields to generate an invoice and give to payer. + /// Provides the necessary information to generate a payable invoice that then may be given to + /// the payer. /// - /// When the invoice is paid the LSP will open a channel to you - /// with the previously agreed upon parameters. - InvoiceGenerationReady { + /// When the invoice is paid, the LSP will open a channel with the previously agreed upon + /// parameters to you. + InvoiceParametersReady { + /// A user-specified identifier used to track the channel open. + /// + /// This is the same value as previously passed to + /// [`LSPS2ClientHandler::select_opening_params`]. + /// + /// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params + user_channel_id: u128, /// The node id of the LSP. counterparty_node_id: PublicKey, /// The intercept short channel id to use in the route hint. @@ -58,12 +56,6 @@ pub enum LSPS2ClientEvent { cltv_expiry_delta: u32, /// The initial payment size you specified. payment_size_msat: Option<u64>, - /// The trust model the LSP expects. - client_trusts_lsp: bool, - /// The `user_channel_id` value passed in to [`LSPS2ClientHandler::create_invoice`]. - /// - /// [`LSPS2ClientHandler::create_invoice`]: crate::lsps2::client::LSPS2ClientHandler::create_invoice - user_channel_id: u128, }, }