diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 558d65e..f93a776 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,11 @@ jobs: - name: Cargo check run: cargo check --release - name: Check documentation - run: cargo doc --release + run: | + cargo doc --release + cargo doc --no-default-features --features no-std + RUSTFLAGS="--cfg lsps1" cargo doc --release + RUSTFLAGS="--cfg lsps1" cargo doc --no-default-features --features no-std - name: Build on Rust ${{ matrix.toolchain }} run: cargo build --verbose --color always - name: Check formatting diff --git a/src/events.rs b/src/events.rs index b3a3a8b..72ab850 100644 --- a/src/events.rs +++ b/src/events.rs @@ -15,6 +15,7 @@ //! //! [`LiquidityManager::get_and_clear_pending_events`]: crate::LiquidityManager::get_and_clear_pending_events +use crate::lsps0; #[cfg(lsps1)] use crate::lsps1; use crate::lsps2; @@ -78,9 +79,16 @@ impl EventQueue { /// An event which you should probably take some action in response to. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { - /// An LSPS2 (JIT Channel) protocol event. - LSPS2(lsps2::LSPS2Event), - /// An LSPS1 protocol event. + /// An LSPS0 client event. + LSPS0Client(lsps0::event::LSPS0ClientEvent), + /// An LSPS1 (Channel Request) client event. #[cfg(lsps1)] - LSPS1(lsps1::event::Event), + LSPS1Client(lsps1::event::LSPS1ClientEvent), + /// An LSPS1 (Channel Request) server event. + #[cfg(lsps1)] + LSPS1Service(lsps1::event::LSPS1ServiceEvent), + /// An LSPS2 (JIT Channel) client event. + LSPS2Client(lsps2::event::LSPS2ClientEvent), + /// An LSPS2 (JIT Channel) server event. + LSPS2Service(lsps2::event::LSPS2ServiceEvent), } diff --git a/src/lib.rs b/src/lib.rs index 11a9d09..58689dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,11 +39,15 @@ mod prelude { } pub mod events; -mod lsps0; +pub mod lsps0; #[cfg(lsps1)] -mod lsps1; +pub mod lsps1; pub mod lsps2; +mod manager; +pub mod message_queue; mod sync; +#[cfg(test)] +mod tests; mod utils; -pub use lsps0::message_handler::{JITChannelsConfig, LiquidityManager, LiquidityProviderConfig}; +pub use manager::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig}; diff --git a/src/lsps0/client.rs b/src/lsps0/client.rs new file mode 100644 index 0000000..a8df49e --- /dev/null +++ b/src/lsps0/client.rs @@ -0,0 +1,158 @@ +//! Contains the main LSPS2 client-side object, [`LSPS0ClientHandler`]. +//! +//! Please refer to the [LSPS0 +//! specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more +//! information. + +use crate::events::{Event, EventQueue}; +use crate::lsps0::event::LSPS0ClientEvent; +use crate::lsps0::msgs::{ + LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsRequest, ListProtocolsResponse, + ProtocolMessageHandler, ResponseError, +}; +use crate::message_queue::MessageQueue; +use crate::sync::Arc; +use crate::utils; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +/// A message handler capable of sending and handling LSPS0 messages. +pub struct LSPS0ClientHandler +where + ES::Target: EntropySource, + MQ::Target: MessageQueue, +{ + entropy_source: ES, + pending_messages: MQ, + pending_events: Arc, +} + +impl LSPS0ClientHandler +where + ES::Target: EntropySource, + MQ::Target: MessageQueue, +{ + /// Returns a new instance of [`LSPS0ClientHandler`]. + pub(crate) fn new( + entropy_source: ES, pending_messages: MQ, pending_events: Arc, + ) -> Self { + Self { entropy_source, pending_messages, pending_events } + } + + /// Calls LSPS0's `list_protocols`. + /// + /// Please refer to the [LSPS0 + /// specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query) + /// for more information. + pub fn list_protocols(&self, counterparty_node_id: &PublicKey) { + let msg = LSPS0Message::Request( + utils::generate_request_id(&self.entropy_source), + LSPS0Request::ListProtocols(ListProtocolsRequest {}), + ); + + self.pending_messages.enqueue(counterparty_node_id, msg.into()); + } + + fn handle_response( + &self, response: LSPS0Response, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match response { + LSPS0Response::ListProtocols(ListProtocolsResponse { protocols }) => { + self.pending_events.enqueue(Event::LSPS0Client( + LSPS0ClientEvent::ListProtocolsResponse { + counterparty_node_id: *counterparty_node_id, + protocols, + }, + )); + Ok(()) + } + LSPS0Response::ListProtocolsError(ResponseError { code, message, data, .. }) => { + Err(LightningError { + err: format!( + "ListProtocols error received. code = {}, message = {}, data = {:?}", + code, message, data + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + } + } + } +} + +impl ProtocolMessageHandler for LSPS0ClientHandler +where + ES::Target: EntropySource, + MQ::Target: MessageQueue, +{ + type ProtocolMessage = LSPS0Message; + const PROTOCOL_NUMBER: Option = None; + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS0Message::Response(_, response) => { + self.handle_response(response, counterparty_node_id) + } + LSPS0Message::Request(..) => { + debug_assert!( + false, + "Client handler received LSPS0 request message. This should never happen." + ); + Err(LightningError { err: format!("Client handler received LSPS0 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + } + } + } +} + +#[cfg(test)] +mod tests { + + use alloc::string::ToString; + use alloc::sync::Arc; + + use crate::lsps0::msgs::{LSPSMessage, RequestId}; + use crate::tests::utils::{TestEntropy, TestMessageQueue}; + + use super::*; + + #[test] + fn test_list_protocols() { + let pending_messages = Arc::new(TestMessageQueue::new()); + let entropy_source = Arc::new(TestEntropy {}); + let event_queue = Arc::new(EventQueue::new()); + + let lsps0_handler = Arc::new(LSPS0ClientHandler::new( + entropy_source, + Arc::clone(&pending_messages), + event_queue, + )); + + let counterparty_node_id = utils::parse_pubkey( + "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190", + ) + .unwrap(); + + lsps0_handler.list_protocols(&counterparty_node_id); + let pending_messages = pending_messages.get_and_clear_pending_msgs(); + + assert_eq!(pending_messages.len(), 1); + + let (pubkey, message) = &pending_messages[0]; + + assert_eq!(*pubkey, counterparty_node_id); + assert_eq!( + *message, + LSPSMessage::LSPS0(LSPS0Message::Request( + RequestId("00000000000000000000000000000000".to_string()), + LSPS0Request::ListProtocols(ListProtocolsRequest {}) + )) + ); + } +} diff --git a/src/lsps0/event.rs b/src/lsps0/event.rs new file mode 100644 index 0000000..163114d --- /dev/null +++ b/src/lsps0/event.rs @@ -0,0 +1,25 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains LSPS0 event types + +use crate::prelude::Vec; +use bitcoin::secp256k1::PublicKey; + +/// An event which an LSPS0 client may want to take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS0ClientEvent { + /// Information from the LSP about the protocols they support. + ListProtocolsResponse { + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// A list of supported protocols. + protocols: Vec, + }, +} diff --git a/src/lsps0/message_handler.rs b/src/lsps0/message_handler.rs deleted file mode 100644 index 43c198b..0000000 --- a/src/lsps0/message_handler.rs +++ /dev/null @@ -1,667 +0,0 @@ -#![allow(missing_docs)] - -#[cfg(lsps1)] -use { - crate::lsps1::channel_manager::CRManager, - crate::lsps1::msgs::{ChannelInfo, OptionsSupported, Order, OrderId, OrderState, Payment}, -}; - -use crate::events::{Event, EventQueue}; -use crate::lsps0::msgs::RequestId; -use crate::lsps0::msgs::{LSPSMessage, RawLSPSMessage, LSPS_MESSAGE_TYPE_ID}; -use crate::lsps0::protocol::LSPS0MessageHandler; -use crate::lsps2::channel_manager::JITChannelManager; -use crate::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; -use crate::prelude::{HashMap, String, ToString, Vec}; -use crate::sync::{Arc, Mutex, RwLock}; - -use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; -use lightning::ln::channelmanager::{AChannelManager, ChainParameters, InterceptId}; -use lightning::ln::features::{InitFeatures, NodeFeatures}; -use lightning::ln::msgs::{ErrorAction, LightningError}; -use lightning::ln::peer_handler::{APeerManager, CustomMessageHandler}; -use lightning::ln::wire::CustomMessageReader; -use lightning::ln::ChannelId; -use lightning::sign::EntropySource; -use lightning::util::errors::APIError; -use lightning::util::logger::Level; -use lightning::util::ser::Readable; - -use bitcoin::blockdata::constants::genesis_block; -use bitcoin::secp256k1::PublicKey; -use bitcoin::BlockHash; - -#[cfg(lsps1)] -use chrono::Utc; - -use core::convert::TryFrom; -use core::ops::Deref; - -const LSPS_FEATURE_BIT: usize = 729; - -/// A trait used to implement a specific LSPS protocol. -/// -/// The messages the protocol uses need to be able to be mapped -/// from and into [`LSPSMessage`]. -pub(crate) trait ProtocolMessageHandler { - type ProtocolMessage: TryFrom + Into; - const PROTOCOL_NUMBER: Option; - - fn handle_message( - &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, - ) -> Result<(), LightningError>; -} - -/// A configuration for [`LiquidityManager`]. -/// -/// Allows end-user to configure options when using the [`LiquidityManager`] -/// to provide liquidity services to clients. -pub struct LiquidityProviderConfig { - /// LSPS1 Configuration - #[cfg(lsps1)] - pub lsps1_config: Option, - /// Optional configuration for JIT channels - /// should you want to support them. - pub lsps2_config: Option, -} - -/// Configuration options for JIT channels. -pub struct JITChannelsConfig { - /// Used to calculate the promise for channel parameters supplied to clients. - /// - /// Note: If this changes then old promises given out will be considered invalid. - pub promise_secret: [u8; 32], - /// The minimum payment size you are willing to accept. - pub min_payment_size_msat: u64, - /// The maximum payment size you are willing to accept. - pub max_payment_size_msat: u64, -} - -#[cfg(lsps1)] -pub struct CRChannelConfig { - pub token: Option, - pub max_fees: Option, - pub options_supported: Option, - pub website: Option, -} - -/// The main interface into LSP functionality. -/// -/// Should be used as a [`CustomMessageHandler`] for your -/// [`PeerManager`]'s [`MessageHandler`]. -/// -/// Should provide a reference to your [`PeerManager`] by calling -/// [`LiquidityManager::set_peer_manager`] post construction. This allows the [`LiquidityManager`] to -/// wake the [`PeerManager`] when there are pending messages to be sent. -/// -/// Users need to continually poll [`LiquidityManager::get_and_clear_pending_events`] in order to surface -/// [`Event`]'s that likely need to be handled. -/// -/// Users must forward the [`Event::HTLCIntercepted`] event parameters to [`LiquidityManager::htlc_intercepted`] -/// and the [`Event::ChannelReady`] event parameters to [`LiquidityManager::channel_ready`]. -/// -/// [`PeerManager`]: lightning::ln::peer_handler::PeerManager -/// [`MessageHandler`]: lightning::ln::peer_handler::MessageHandler -/// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted -/// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady -pub struct LiquidityManager< - ES: Deref + Clone, - CM: Deref + Clone, - PM: Deref + Clone, - C: Deref + Clone, -> where - ES::Target: EntropySource, - CM::Target: AChannelManager, - PM::Target: APeerManager, - C::Target: Filter, -{ - pending_messages: Arc>>, - pending_events: Arc, - request_id_to_method_map: Mutex>, - lsps0_message_handler: LSPS0MessageHandler, - #[cfg(lsps1)] - lsps1_message_handler: Option>, - lsps2_message_handler: Option>, - provider_config: Option, - channel_manager: CM, - chain_source: Option, - genesis_hash: Option, - best_block: Option>, -} - -impl - LiquidityManager -where - ES::Target: EntropySource, - CM::Target: AChannelManager, - PM::Target: APeerManager, - C::Target: Filter, -{ - /// Constructor for the [`LiquidityManager`]. - /// - /// Sets up the required protocol message handlers based on the given [`LiquidityProviderConfig`]. - pub fn new( - entropy_source: ES, provider_config: Option, channel_manager: CM, - chain_source: Option, chain_params: Option, - ) -> Self -where { - let pending_messages = Arc::new(Mutex::new(vec![])); - let pending_events = Arc::new(EventQueue::new()); - - let lsps0_message_handler = LSPS0MessageHandler::new( - entropy_source.clone().clone(), - vec![], - Arc::clone(&pending_messages), - ); - - let lsps2_message_handler = provider_config.as_ref().and_then(|config| { - config.lsps2_config.as_ref().map(|config| { - JITChannelManager::new( - entropy_source.clone(), - config, - Arc::clone(&pending_messages), - Arc::clone(&pending_events), - channel_manager.clone(), - ) - }) - }); - - #[cfg(lsps1)] - let lsps1_message_handler = provider_config.as_ref().and_then(|config| { - config.lsps1_config.as_ref().map(|lsps1_config| { - CRManager::new( - entropy_source.clone(), - lsps1_config, - Arc::clone(&pending_messages), - Arc::clone(&pending_events), - channel_manager.clone(), - chain_source.clone(), - ) - }) - }); - - Self { - pending_messages, - pending_events, - request_id_to_method_map: Mutex::new(HashMap::new()), - lsps0_message_handler, - #[cfg(lsps1)] - lsps1_message_handler, - lsps2_message_handler, - provider_config, - channel_manager, - chain_source, - genesis_hash: chain_params - .map(|chain_params| genesis_block(chain_params.network).header.block_hash()), - best_block: chain_params.map(|chain_params| RwLock::new(chain_params.best_block)), - } - } - - /// Blocks the current thread until next event is ready and returns it. - /// - /// Typically you would spawn a thread or task that calls this in a loop. - #[cfg(feature = "std")] - pub fn wait_next_event(&self) -> Event { - self.pending_events.wait_next_event() - } - - /// Returns `Some` if an event is ready. - /// - /// Typically you would spawn a thread or task that calls this in a loop. - pub fn next_event(&self) -> Option { - self.pending_events.next_event() - } - - /// Returns and clears all events without blocking. - /// - /// Typically you would spawn a thread or task that calls this in a loop. - pub fn get_and_clear_pending_events(&self) -> Vec { - self.pending_events.get_and_clear_pending_events() - } - - /// Set a [`PeerManager`] reference for the message handlers. - /// - /// This allows the message handlers to wake the [`PeerManager`] by calling - /// [`PeerManager::process_events`] after enqueing messages to be sent. - /// - /// Without this the messages will be sent based on whatever polling interval - /// your background processor uses. - /// - /// [`PeerManager`]: lightning::ln::peer_handler::PeerManager - /// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events - pub fn set_peer_manager(&self, peer_manager: PM) { - #[cfg(lsps1)] - if let Some(lsps1_message_handler) = &self.lsps1_message_handler { - lsps1_message_handler.set_peer_manager(peer_manager.clone()); - } - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.set_peer_manager(peer_manager); - } - } - - #[cfg(lsps1)] - pub fn request_for_info( - &self, counterparty_node_id: PublicKey, channel_id: u128, - ) -> Result<(), APIError> { - if let Some(lsps1_message_handler) = &self.lsps1_message_handler { - lsps1_message_handler.request_for_info(counterparty_node_id, channel_id); - } - Ok(()) - } - - #[cfg(lsps1)] - pub fn place_order( - &self, channel_id: u128, counterparty_node_id: &PublicKey, order: Order, - ) -> Result<(), APIError> { - if let Some(lsps1_message_handler) = &self.lsps1_message_handler { - lsps1_message_handler.place_order(channel_id, counterparty_node_id, order)?; - } - Ok(()) - } - - #[cfg(lsps1)] - pub fn send_invoice_for_order( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, payment: Payment, - created_at: chrono::DateTime, expires_at: chrono::DateTime, - ) -> Result<(), APIError> { - if let Some(lsps1_message_handler) = &self.lsps1_message_handler { - lsps1_message_handler.send_invoice_for_order( - request_id, - counterparty_node_id, - payment, - created_at, - expires_at, - )?; - } - Ok(()) - } - - #[cfg(lsps1)] - pub fn check_order_status( - self, channel_id: u128, counterparty_node_id: &PublicKey, order_id: OrderId, - ) -> Result<(), APIError> { - if let Some(lsps1_message_handler) = &self.lsps1_message_handler { - lsps1_message_handler.check_order_status(counterparty_node_id, order_id, channel_id)?; - } - Ok(()) - } - - #[cfg(lsps1)] - pub fn update_order_status( - &self, request_id: RequestId, counterparty_node_id: PublicKey, order_id: OrderId, - order_state: OrderState, channel: Option, - ) -> Result<(), APIError> { - if let Some(lsps1_message_handler) = &self.lsps1_message_handler { - lsps1_message_handler.update_order_status( - request_id, - counterparty_node_id, - order_id, - order_state, - channel, - )?; - } - Ok(()) - } - - /// Initiate the creation of an invoice that when paid will open a channel - /// with enough inbound liquidity to be able to receive the payment. - /// - /// `counterparty_node_id` is the node_id of the LSP you would like to use. - /// - /// 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. - /// - /// `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 lsps2_create_invoice( - &self, counterparty_node_id: PublicKey, payment_size_msat: Option, - token: Option, user_channel_id: u128, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.create_invoice( - counterparty_node_id, - payment_size_msat, - token, - user_channel_id, - ); - Ok(()) - } else { - Err(APIError::APIMisuseError { - err: "JIT Channels were not configured when LSPManager was instantiated" - .to_string(), - }) - } - } - - /// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid. - /// - /// Should be called in response to receiving a [`LSPS2Event::GetInfo`] event. - /// - /// [`LSPS2Event::GetInfo`]: crate::lsps2::LSPS2Event::GetInfo - pub fn invalid_token_provided( - &self, counterparty_node_id: PublicKey, request_id: RequestId, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.invalid_token_provided(counterparty_node_id, request_id) - } else { - Err(APIError::APIMisuseError { - err: "JIT Channels were not configured when LSPManager was instantiated" - .to_string(), - }) - } - } - - /// Used by LSP to provide fee parameters to a client requesting a JIT Channel. - /// - /// Should be called in response to receiving a [`LSPS2Event::GetInfo`] event. - /// - /// [`LSPS2Event::GetInfo`]: crate::lsps2::LSPS2Event::GetInfo - pub fn opening_fee_params_generated( - &self, counterparty_node_id: PublicKey, request_id: RequestId, - opening_fee_params_menu: Vec, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.opening_fee_params_generated( - counterparty_node_id, - request_id, - opening_fee_params_menu, - ) - } else { - Err(APIError::APIMisuseError { - err: "JIT Channels were not configured when LSPManager was instantiated" - .to_string(), - }) - } - } - - /// Used by client to confirm which channel parameters to use for the JIT Channel buy request. - /// 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 [`LSPS2Event::GetInfoResponse`] event. - /// - /// [`LSPS2Event::GetInfoResponse`]: crate::lsps2::LSPS2Event::GetInfoResponse - pub fn opening_fee_params_selected( - &self, counterparty_node_id: PublicKey, channel_id: u128, - opening_fee_params: OpeningFeeParams, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.opening_fee_params_selected( - counterparty_node_id, - channel_id, - opening_fee_params, - ) - } else { - Err(APIError::APIMisuseError { - err: "JIT Channels were not configured when LSPManager was instantiated" - .to_string(), - }) - } - } - - /// Used by LSP to provide client with the scid and cltv_expiry_delta to use in their invoice. - /// - /// Should be called in response to receiving a [`LSPS2Event::BuyRequest`] event. - /// - /// [`LSPS2Event::BuyRequest`]: crate::lsps2::LSPS2Event::BuyRequest - pub fn invoice_parameters_generated( - &self, counterparty_node_id: PublicKey, request_id: RequestId, scid: u64, - cltv_expiry_delta: u32, client_trusts_lsp: bool, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.invoice_parameters_generated( - counterparty_node_id, - request_id, - scid, - cltv_expiry_delta, - client_trusts_lsp, - ) - } else { - Err(APIError::APIMisuseError { - err: "JIT Channels were not configured when LSPManager was instantiated" - .to_string(), - }) - } - } - - /// Forward [`Event::HTLCIntercepted`] event parameters into this function. - /// - /// Will fail the intercepted HTLC if the scid matches a payment we are expecting - /// but the payment amount is incorrect or the expiry has passed. - /// - /// Will generate a [`LSPS2Event::OpenChannel`] event if the scid matches a payment we are expected - /// and the payment amount is correct and the offer has not expired. - /// - /// Will do nothing if the scid does not match any of the ones we gave out. - /// - /// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted - /// [`LSPS2Event::OpenChannel`]: crate::lsps2::LSPS2Event::OpenChannel - pub fn htlc_intercepted( - &self, scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.htlc_intercepted( - scid, - intercept_id, - expected_outbound_amount_msat, - )?; - } - - Ok(()) - } - - /// Forward [`Event::ChannelReady`] event parameters into this function. - /// - /// Will forward the intercepted HTLC if it matches a channel - /// we need to forward a payment over otherwise it will be ignored. - /// - /// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady - pub fn channel_ready( - &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - ) -> Result<(), APIError> { - if let Some(lsps2_message_handler) = &self.lsps2_message_handler { - lsps2_message_handler.channel_ready( - user_channel_id, - channel_id, - counterparty_node_id, - )?; - } - - Ok(()) - } - - fn handle_lsps_message( - &self, msg: LSPSMessage, sender_node_id: &PublicKey, - ) -> Result<(), lightning::ln::msgs::LightningError> { - match msg { - LSPSMessage::Invalid => { - return Err(LightningError { err: format!("{} did not understand a message we previously sent, maybe they don't support a protocol we are trying to use?", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Error)}); - } - LSPSMessage::LSPS0(msg) => { - self.lsps0_message_handler.handle_message(msg, sender_node_id)?; - } - #[cfg(lsps1)] - LSPSMessage::LSPS1(msg) => match &self.lsps1_message_handler { - Some(lsps1_message_handler) => { - lsps1_message_handler.handle_message(msg, sender_node_id)?; - } - None => { - return Err(LightningError { err: format!("Received LSPS1 message without LSPS1 message handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); - } - }, - LSPSMessage::LSPS2(msg) => match &self.lsps2_message_handler { - Some(lsps2_message_handler) => { - lsps2_message_handler.handle_message(msg, sender_node_id)?; - } - None => { - return Err(LightningError { err: format!("Received LSPS2 message without LSPS2 message handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); - } - }, - } - Ok(()) - } - - fn enqueue_message(&self, node_id: PublicKey, msg: LSPSMessage) { - let mut pending_msgs = self.pending_messages.lock().unwrap(); - pending_msgs.push((node_id, msg)); - } -} - -impl - CustomMessageReader for LiquidityManager -where - ES::Target: EntropySource, - CM::Target: AChannelManager, - PM::Target: APeerManager, - C::Target: Filter, -{ - type CustomMessage = RawLSPSMessage; - - fn read( - &self, message_type: u16, buffer: &mut RD, - ) -> Result, lightning::ln::msgs::DecodeError> { - match message_type { - LSPS_MESSAGE_TYPE_ID => Ok(Some(RawLSPSMessage::read(buffer)?)), - _ => Ok(None), - } - } -} - -impl CustomMessageHandler - for LiquidityManager -where - ES::Target: EntropySource, - CM::Target: AChannelManager, - PM::Target: APeerManager, - C::Target: Filter, -{ - fn handle_custom_message( - &self, msg: Self::CustomMessage, sender_node_id: &PublicKey, - ) -> Result<(), lightning::ln::msgs::LightningError> { - let message = { - let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); - LSPSMessage::from_str_with_id_map(&msg.payload, &mut request_id_to_method_map) - }; - - match message { - Ok(msg) => self.handle_lsps_message(msg, sender_node_id), - Err(_) => { - self.enqueue_message(*sender_node_id, LSPSMessage::Invalid); - Ok(()) - } - } - } - - fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Self::CustomMessage)> { - let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); - self.pending_messages - .lock() - .unwrap() - .drain(..) - .map(|(public_key, lsps_message)| { - if let Some((request_id, method_name)) = lsps_message.get_request_id_and_method() { - request_id_to_method_map.insert(request_id, method_name); - } - ( - public_key, - RawLSPSMessage { payload: serde_json::to_string(&lsps_message).unwrap() }, - ) - }) - .collect() - } - - fn provided_node_features(&self) -> NodeFeatures { - let mut features = NodeFeatures::empty(); - - if self.provider_config.is_some() { - features.set_optional_custom_bit(LSPS_FEATURE_BIT).unwrap(); - } - - features - } - - fn provided_init_features(&self, _their_node_id: &PublicKey) -> InitFeatures { - let mut features = InitFeatures::empty(); - - if self.provider_config.is_some() { - features.set_optional_custom_bit(LSPS_FEATURE_BIT).unwrap(); - } - - features - } -} - -impl Listen - for LiquidityManager -where - ES::Target: EntropySource, - CM::Target: AChannelManager, - PM::Target: APeerManager, - C::Target: Filter, -{ - fn filtered_block_connected( - &self, header: &bitcoin::BlockHeader, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - if let Some(best_block) = &self.best_block { - let best_block = best_block.read().unwrap(); - assert_eq!(best_block.block_hash(), header.prev_blockhash, - "Blocks must be connected in chain-order - the connected header must build on the last connected header"); - assert_eq!(best_block.height(), height - 1, - "Blocks must be connected in chain-order - the connected block height must be one greater than the previous height"); - } - - self.transactions_confirmed(header, txdata, height); - self.best_block_updated(header, height); - } - - fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) { - let new_height = height - 1; - if let Some(best_block) = &self.best_block { - let mut best_block = best_block.write().unwrap(); - assert_eq!(best_block.block_hash(), header.block_hash(), - "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); - assert_eq!(best_block.height(), height, - "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); - *best_block = BestBlock::new(header.prev_blockhash, new_height) - } - - // TODO: Call block_disconnected on all sub-modules that require it, e.g., CRManager. - // Internally this should call transaction_unconfirmed for all transactions that were - // confirmed at a height <= the one we now disconnected. - } -} - -impl Confirm - for LiquidityManager -where - ES::Target: EntropySource, - CM::Target: AChannelManager, - PM::Target: APeerManager, - C::Target: Filter, -{ - fn transactions_confirmed( - &self, header: &bitcoin::BlockHeader, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - // TODO: Call transactions_confirmed on all sub-modules that require it, e.g., CRManager. - } - - fn transaction_unconfirmed(&self, txid: &bitcoin::Txid) { - // TODO: Call transaction_unconfirmed on all sub-modules that require it, e.g., CRManager. - // Internally this should call transaction_unconfirmed for all transactions that were - // confirmed at a height <= the one we now unconfirmed. - } - - fn best_block_updated(&self, header: &bitcoin::BlockHeader, height: u32) { - // TODO: Call best_block_updated on all sub-modules that require it, e.g., CRManager. - } - - fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, Option)> { - // TODO: Collect relevant txids from all sub-modules that, e.g., CRManager. - Vec::new() - } -} diff --git a/src/lsps0/mod.rs b/src/lsps0/mod.rs index 00185eb..f4f8a56 100644 --- a/src/lsps0/mod.rs +++ b/src/lsps0/mod.rs @@ -9,6 +9,7 @@ //! Types and primitives that implement the LSPS0: Transport Layer specification. -pub mod message_handler; +pub mod client; +pub mod event; pub mod msgs; -pub mod protocol; +pub mod service; diff --git a/src/lsps0/msgs.rs b/src/lsps0/msgs.rs index 6630111..2d51b69 100644 --- a/src/lsps0/msgs.rs +++ b/src/lsps0/msgs.rs @@ -1,3 +1,7 @@ +//! Contains basic data types that allow for the (de-)seralization of LSPS messages in the JSON-RPC 2.0 format. +//! +//! Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more information. + #[cfg(lsps1)] use crate::lsps1::msgs::{ LSPS1Message, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_METHOD_NAME, @@ -10,8 +14,11 @@ use crate::lsps2::msgs::{ use crate::prelude::{HashMap, String, ToString, Vec}; use lightning::impl_writeable_msg; +use lightning::ln::msgs::LightningError; use lightning::ln::wire; +use bitcoin::secp256k1::PublicKey; + use serde::de; use serde::de::{MapAccess, Visitor}; use serde::ser::SerializeStruct; @@ -36,6 +43,19 @@ const LSPS0_LISTPROTOCOLS_METHOD_NAME: &str = "lsps0.list_protocols"; /// The Lightning message type id for LSPS messages. pub const LSPS_MESSAGE_TYPE_ID: u16 = 37913; +/// A trait used to implement a specific LSPS protocol. +/// +/// The messages the protocol uses need to be able to be mapped +/// from and into [`LSPSMessage`]. +pub(crate) trait ProtocolMessageHandler { + type ProtocolMessage: TryFrom + Into; + const PROTOCOL_NUMBER: Option; + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError>; +} + /// Lightning message type used by LSPS protocols. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RawLSPSMessage { @@ -51,30 +71,56 @@ impl wire::Type for RawLSPSMessage { } } +/// A JSON-RPC request's `id`. +/// +/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#request_object) for +/// more information. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RequestId(pub String); +/// An error returned in response to an JSON-RPC request. +/// +/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#error_object) for +/// more information. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct ResponseError { + /// A number that indicates the error type that occurred. pub code: i32, + /// A string providing a short description of the error. pub message: String, + /// A primitive or structured value that contains additional information about the error. pub data: Option, } +/// A `list_protocols` request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct ListProtocolsRequest {} +/// A response to a `list_protocols` request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct ListProtocolsResponse { + /// A list of supported protocols. pub protocols: Vec, } +/// An LSPS0 protocol request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS0Request { + /// A request calling `list_protocols`. ListProtocols(ListProtocolsRequest), } impl LSPS0Request { + /// Returns the method name associated with the given request variant. pub fn method(&self) -> &str { match self { LSPS0Request::ListProtocols(_) => LSPS0_LISTPROTOCOLS_METHOD_NAME, @@ -82,15 +128,27 @@ impl LSPS0Request { } } +/// An LSPS0 protocol request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS0Response { + /// A response to a `list_protocols` request. ListProtocols(ListProtocolsResponse), + /// An error response to a `list_protocols` request. ListProtocolsError(ResponseError), } +/// An LSPS0 protocol message. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS0Message { + /// A request variant. Request(RequestId, LSPS0Request), + /// A response variant. Response(RequestId, LSPS0Response), } @@ -114,16 +172,25 @@ impl From for LSPSMessage { } } +/// A (de-)serializable LSPS message allowing to be sent over the wire. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPSMessage { + /// An invalid variant. Invalid, + /// An LSPS0 message. LSPS0(LSPS0Message), + /// An LSPS1 message. #[cfg(lsps1)] LSPS1(LSPS1Message), + /// An LSPS2 message. LSPS2(LSPS2Message), } impl LSPSMessage { + /// A constructor returning an `LSPSMessage` from a raw JSON string. + /// + /// The given `request_id_to_method` associates request ids with method names, as response objects + /// don't carry the latter. pub fn from_str_with_id_map( json_str: &str, request_id_to_method: &mut HashMap, ) -> Result { @@ -132,6 +199,7 @@ impl LSPSMessage { deserializer.deserialize_any(visitor) } + /// Returns the request id and the method. pub fn get_request_id_and_method(&self) -> Option<(String, String)> { match self { LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => { diff --git a/src/lsps0/protocol.rs b/src/lsps0/protocol.rs deleted file mode 100644 index 2a3e650..0000000 --- a/src/lsps0/protocol.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::lsps0::message_handler::ProtocolMessageHandler; -use crate::lsps0::msgs::{ - LSPS0Message, LSPS0Request, LSPS0Response, LSPSMessage, ListProtocolsRequest, - ListProtocolsResponse, RequestId, ResponseError, -}; -use crate::prelude::Vec; -use crate::sync::{Arc, Mutex}; -use crate::utils; - -use lightning::ln::msgs::{ErrorAction, LightningError}; -use lightning::sign::EntropySource; -use lightning::util::logger::Level; - -use bitcoin::secp256k1::PublicKey; - -use core::ops::Deref; - -pub struct LSPS0MessageHandler -where - ES::Target: EntropySource, -{ - entropy_source: ES, - pending_messages: Arc>>, - protocols: Vec, -} - -impl LSPS0MessageHandler -where - ES::Target: EntropySource, -{ - pub fn new( - entropy_source: ES, protocols: Vec, - pending_messages: Arc>>, - ) -> Self { - Self { entropy_source, protocols, pending_messages } - } - - pub fn list_protocols(&self, counterparty_node_id: PublicKey) { - let msg = LSPS0Message::Request( - utils::generate_request_id(&self.entropy_source), - LSPS0Request::ListProtocols(ListProtocolsRequest {}), - ); - - self.enqueue_message(counterparty_node_id, msg); - } - - fn enqueue_message(&self, counterparty_node_id: PublicKey, message: LSPS0Message) { - self.pending_messages.lock().unwrap().push((counterparty_node_id, message.into())); - } - - fn handle_request( - &self, request_id: RequestId, request: LSPS0Request, counterparty_node_id: &PublicKey, - ) -> Result<(), lightning::ln::msgs::LightningError> { - match request { - LSPS0Request::ListProtocols(_) => { - let msg = LSPS0Message::Response( - request_id, - LSPS0Response::ListProtocols(ListProtocolsResponse { - protocols: self.protocols.clone(), - }), - ); - self.enqueue_message(*counterparty_node_id, msg); - Ok(()) - } - } - } - - fn handle_response( - &self, response: LSPS0Response, counterparty_node_id: &PublicKey, - ) -> Result<(), LightningError> { - match response { - LSPS0Response::ListProtocols(ListProtocolsResponse { protocols }) => Ok(()), - LSPS0Response::ListProtocolsError(ResponseError { code, message, data, .. }) => { - Err(LightningError { - err: format!( - "ListProtocols error received. code = {}, message = {}, data = {:?}", - code, message, data - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }) - } - } - } -} - -impl ProtocolMessageHandler for LSPS0MessageHandler -where - ES::Target: EntropySource, -{ - type ProtocolMessage = LSPS0Message; - const PROTOCOL_NUMBER: Option = None; - - fn handle_message( - &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, - ) -> Result<(), LightningError> { - match message { - LSPS0Message::Request(request_id, request) => { - self.handle_request(request_id, request, counterparty_node_id) - } - LSPS0Message::Response(_, response) => { - self.handle_response(response, counterparty_node_id) - } - } - } -} - -#[cfg(test)] -mod tests { - - use alloc::string::ToString; - use alloc::sync::Arc; - - use super::*; - - struct TestEntropy {} - impl EntropySource for TestEntropy { - fn get_secure_random_bytes(&self) -> [u8; 32] { - [0; 32] - } - } - - #[test] - fn test_handle_list_protocols_request() { - let entropy = Arc::new(TestEntropy {}); - let protocols: Vec = vec![]; - let pending_messages = Arc::new(Mutex::new(vec![])); - - let lsps0_handler = - Arc::new(LSPS0MessageHandler::new(entropy, protocols, pending_messages.clone())); - - let list_protocols_request = LSPS0Message::Request( - RequestId("xyz123".to_string()), - LSPS0Request::ListProtocols(ListProtocolsRequest {}), - ); - let counterparty_node_id = utils::parse_pubkey( - "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190", - ) - .unwrap(); - - lsps0_handler.handle_message(list_protocols_request, &counterparty_node_id).unwrap(); - let pending_messages = pending_messages.lock().unwrap(); - - assert_eq!(pending_messages.len(), 1); - - let (pubkey, message) = &pending_messages[0]; - - assert_eq!(*pubkey, counterparty_node_id); - assert_eq!( - *message, - LSPSMessage::LSPS0(LSPS0Message::Response( - RequestId("xyz123".to_string()), - LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![] }) - )) - ); - } - - #[test] - fn test_list_protocols() { - let pending_messages = Arc::new(Mutex::new(vec![])); - - let lsps0_handler = Arc::new(LSPS0MessageHandler::new( - Arc::new(TestEntropy {}), - vec![1, 2, 3], - pending_messages.clone(), - )); - - let counterparty_node_id = utils::parse_pubkey( - "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190", - ) - .unwrap(); - - lsps0_handler.list_protocols(counterparty_node_id); - let pending_messages = pending_messages.lock().unwrap(); - - assert_eq!(pending_messages.len(), 1); - - let (pubkey, message) = &pending_messages[0]; - - assert_eq!(*pubkey, counterparty_node_id); - assert_eq!( - *message, - LSPSMessage::LSPS0(LSPS0Message::Request( - RequestId("00000000000000000000000000000000".to_string()), - LSPS0Request::ListProtocols(ListProtocolsRequest {}) - )) - ); - } -} diff --git a/src/lsps0/service.rs b/src/lsps0/service.rs new file mode 100644 index 0000000..403033e --- /dev/null +++ b/src/lsps0/service.rs @@ -0,0 +1,134 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS0 server-side object, [`LSPS0ServiceHandler`]. +//! +//! Please refer to the [LSPS0 +//! specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more +//! information. + +use crate::lsps0::msgs::{ + LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsResponse, ProtocolMessageHandler, + RequestId, +}; +use crate::message_queue::MessageQueue; +use crate::prelude::Vec; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +/// The main server-side object allowing to send and receive LSPS0 messages. +pub struct LSPS0ServiceHandler +where + MQ::Target: MessageQueue, +{ + pending_messages: MQ, + protocols: Vec, +} + +impl LSPS0ServiceHandler +where + MQ::Target: MessageQueue, +{ + /// Returns a new instance of [`LSPS0ServiceHandler`]. + pub(crate) fn new(protocols: Vec, pending_messages: MQ) -> Self { + Self { protocols, pending_messages } + } + + fn handle_request( + &self, request_id: RequestId, request: LSPS0Request, counterparty_node_id: &PublicKey, + ) -> Result<(), lightning::ln::msgs::LightningError> { + match request { + LSPS0Request::ListProtocols(_) => { + let msg = LSPS0Message::Response( + request_id, + LSPS0Response::ListProtocols(ListProtocolsResponse { + protocols: self.protocols.clone(), + }), + ); + self.pending_messages.enqueue(counterparty_node_id, msg.into()); + Ok(()) + } + } + } +} + +impl ProtocolMessageHandler for LSPS0ServiceHandler +where + MQ::Target: MessageQueue, +{ + type ProtocolMessage = LSPS0Message; + const PROTOCOL_NUMBER: Option = None; + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS0Message::Request(request_id, request) => { + self.handle_request(request_id, request, counterparty_node_id) + } + LSPS0Message::Response(..) => { + debug_assert!( + false, + "Service handler received LSPS0 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS0 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + } + } + } +} + +#[cfg(test)] +mod tests { + + use crate::lsps0::msgs::{LSPSMessage, ListProtocolsRequest}; + use crate::tests::utils::TestMessageQueue; + use crate::utils; + use alloc::string::ToString; + use alloc::sync::Arc; + + use super::*; + + #[test] + fn test_handle_list_protocols_request() { + let protocols: Vec = vec![]; + let pending_messages = Arc::new(TestMessageQueue::new()); + + let lsps0_handler = Arc::new(LSPS0ServiceHandler::new(protocols, pending_messages.clone())); + + let list_protocols_request = LSPS0Message::Request( + RequestId("xyz123".to_string()), + LSPS0Request::ListProtocols(ListProtocolsRequest {}), + ); + let counterparty_node_id = utils::parse_pubkey( + "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190", + ) + .unwrap(); + + lsps0_handler.handle_message(list_protocols_request, &counterparty_node_id).unwrap(); + let pending_messages = pending_messages.get_and_clear_pending_msgs(); + + assert_eq!(pending_messages.len(), 1); + + let (pubkey, message) = &pending_messages[0]; + + assert_eq!(*pubkey, counterparty_node_id); + assert_eq!( + *message, + LSPSMessage::LSPS0(LSPS0Message::Response( + RequestId("xyz123".to_string()), + LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![] }) + )) + ); + } +} diff --git a/src/lsps1/channel_manager.rs b/src/lsps1/client.rs similarity index 52% rename from src/lsps1/channel_manager.rs rename to src/lsps1/client.rs index 007075a..6b0081f 100644 --- a/src/lsps1/channel_manager.rs +++ b/src/lsps1/client.rs @@ -7,37 +7,43 @@ // You may not use this file except in accordance with one or both of these // licenses. +//! Contains the main LSPS1 client object, [`LSPS1ClientHandler`]. + +use super::event::LSPS1ClientEvent; use super::msgs::{ - ChannelInfo, CreateOrderRequest, CreateOrderResponse, GetInfoRequest, GetInfoResponse, - GetOrderRequest, GetOrderResponse, LSPS1Message, LSPS1Request, LSPS1Response, OptionsSupported, - Order, OrderId, OrderState, Payment, LSPS1_CREATE_ORDER_REQUEST_INVALID_VERSION_ERROR_CODE, - LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, + CreateOrderRequest, CreateOrderResponse, GetInfoRequest, GetInfoResponse, GetOrderRequest, + GetOrderResponse, LSPS1Message, LSPS1Request, LSPS1Response, OptionsSupported, OrderId, + OrderParams, }; use super::utils::is_valid; +use crate::message_queue::MessageQueue; use crate::events::EventQueue; -use crate::lsps0::message_handler::{CRChannelConfig, ProtocolMessageHandler}; -use crate::lsps0::msgs::{LSPSMessage, RequestId}; +use crate::lsps0::msgs::{ProtocolMessageHandler, RequestId}; use crate::prelude::{HashMap, String, ToString, Vec}; use crate::sync::{Arc, Mutex, RwLock}; -use crate::utils; use crate::{events::Event, lsps0::msgs::ResponseError}; use lightning::chain::Filter; use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; -use lightning::ln::peer_handler::APeerManager; use lightning::sign::EntropySource; use lightning::util::errors::APIError; use lightning::util::logger::Level; use bitcoin::secp256k1::PublicKey; -use chrono::Utc; use core::ops::Deref; const SUPPORTED_SPEC_VERSIONS: [u16; 1] = [1]; +/// Client-side configuration options for LSPS1 channel requests. +#[derive(Clone, Debug)] +pub struct LSPS1ClientConfig { + /// The maximally allowed channel fees. + pub max_channel_fees_msat: Option, +} + struct ChannelStateError(String); impl From for LightningError { @@ -50,7 +56,7 @@ impl From for LightningError { enum InboundRequestState { InfoRequested, OptionsSupport { version: u16, options_supported: OptionsSupported }, - OrderRequested { version: u16, order: Order }, + OrderRequested { version: u16, order: OrderParams }, PendingPayment { order_id: OrderId }, AwaitingConfirmation { id: u128, order_id: OrderId }, } @@ -81,7 +87,7 @@ impl InboundRequestState { } } - pub fn order_requested(&self, order: Order) -> Result { + fn order_requested(&self, order: OrderParams) -> Result { match self { InboundRequestState::OptionsSupport { version, options_supported } => { if is_valid(&order, options_supported) { @@ -100,8 +106,8 @@ impl InboundRequestState { } } - pub fn order_received( - &self, response_order: &Order, order_id: OrderId, + fn order_received( + &self, response_order: &OrderParams, order_id: OrderId, ) -> Result { match self { InboundRequestState::OrderRequested { version, order } => { @@ -121,7 +127,7 @@ impl InboundRequestState { } } - pub fn pay_for_channel(&self, channel_id: u128) -> Result { + fn pay_for_channel(&self, channel_id: u128) -> Result { match self { InboundRequestState::PendingPayment { order_id } => { Ok(InboundRequestState::AwaitingConfirmation { @@ -143,11 +149,11 @@ struct InboundCRChannel { } impl InboundCRChannel { - pub fn new(id: u128) -> Self { + fn new(id: u128) -> Self { Self { id, state: InboundRequestState::InfoRequested } } - pub fn info_received( + fn info_received( &mut self, versions: Vec, options: OptionsSupported, ) -> Result { self.state = self.state.info_received(versions, options)?; @@ -161,7 +167,7 @@ impl InboundCRChannel { } } - pub fn order_requested(&mut self, order: Order) -> Result { + fn order_requested(&mut self, order: OrderParams) -> Result { self.state = self.state.order_requested(order)?; match self.state { @@ -175,155 +181,81 @@ impl InboundCRChannel { } } - pub fn order_received( - &mut self, order: &Order, order_id: OrderId, + fn order_received( + &mut self, order: &OrderParams, order_id: OrderId, ) -> Result<(), LightningError> { self.state = self.state.order_received(order, order_id)?; Ok(()) } - pub fn pay_for_channel(&mut self, channel_id: u128) -> Result<(), LightningError> { + fn pay_for_channel(&mut self, channel_id: u128) -> Result<(), LightningError> { self.state = self.state.pay_for_channel(channel_id)?; Ok(()) } } -#[derive(PartialEq, Debug)] -enum OutboundRequestState { - OrderCreated { order_id: OrderId }, - WaitingPayment { order_id: OrderId }, - Ready, -} - -impl OutboundRequestState { - pub fn create_payment_invoice(&self) -> Result { - match self { - OutboundRequestState::OrderCreated { order_id } => { - Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) - } - state => Err(ChannelStateError(format!( - "Received unexpected get_versions response. JIT Channel was in state: {:?}", - state - ))), - } - } -} - -struct OutboundCRChannelConfig { - order: Order, - created_at: chrono::DateTime, - expires_at: chrono::DateTime, - payment: Payment, -} - -struct OutboundCRChannel { - state: OutboundRequestState, - config: OutboundCRChannelConfig, -} - -impl OutboundCRChannel { - pub fn new( - order: Order, created_at: chrono::DateTime, expires_at: chrono::DateTime, - order_id: OrderId, payment: Payment, - ) -> Self { - Self { - state: OutboundRequestState::OrderCreated { order_id }, - config: OutboundCRChannelConfig { order, created_at, expires_at, payment }, - } - } - pub fn create_payment_invoice(&mut self) -> Result<(), LightningError> { - self.state = self.state.create_payment_invoice()?; - Ok(()) - } - - pub fn check_order_validity(&self, options_supported: &OptionsSupported) -> bool { - let order = &self.config.order; - - is_valid(order, options_supported) - } -} - #[derive(Default)] struct PeerState { inbound_channels_by_id: HashMap, - outbound_channels_by_order_id: HashMap, request_to_cid: HashMap, pending_requests: HashMap, } impl PeerState { - pub fn insert_inbound_channel(&mut self, id: u128, channel: InboundCRChannel) { + fn insert_inbound_channel(&mut self, id: u128, channel: InboundCRChannel) { self.inbound_channels_by_id.insert(id, channel); } - pub fn insert_outbound_channel(&mut self, order_id: OrderId, channel: OutboundCRChannel) { - self.outbound_channels_by_order_id.insert(order_id, channel); - } - - pub fn insert_request(&mut self, request_id: RequestId, channel_id: u128) { + fn insert_request(&mut self, request_id: RequestId, channel_id: u128) { self.request_to_cid.insert(request_id, channel_id); } - pub fn remove_inbound_channel(&mut self, id: u128) { + fn remove_inbound_channel(&mut self, id: u128) { self.inbound_channels_by_id.remove(&id); } - - pub fn remove_outbound_channel(&mut self, order_id: OrderId) { - self.outbound_channels_by_order_id.remove(&order_id); - } } -pub struct CRManager +/// The main object allowing to send and receive LSPS1 messages. +pub struct LSPS1ClientHandler where ES::Target: EntropySource, CM::Target: AChannelManager, - PM::Target: APeerManager, + MQ::Target: MessageQueue, C::Target: Filter, { entropy_source: ES, channel_manager: CM, - peer_manager: Mutex>, chain_source: Option, - pending_messages: Arc>>, + pending_messages: MQ, pending_events: Arc, per_peer_state: RwLock>>, - options_config: Option, - website: Option, - max_fees: Option, + config: LSPS1ClientConfig, } -impl CRManager +impl LSPS1ClientHandler where ES::Target: EntropySource, CM::Target: AChannelManager, - PM::Target: APeerManager, + MQ::Target: MessageQueue, C::Target: Filter, ES::Target: EntropySource, { pub(crate) fn new( - entropy_source: ES, config: &CRChannelConfig, - pending_messages: Arc>>, - pending_events: Arc, channel_manager: CM, chain_source: Option, + entropy_source: ES, pending_messages: MQ, pending_events: Arc, + channel_manager: CM, chain_source: Option, config: LSPS1ClientConfig, ) -> Self { Self { entropy_source, channel_manager, - peer_manager: Mutex::new(None), chain_source, pending_messages, pending_events, per_peer_state: RwLock::new(HashMap::new()), - options_config: config.options_supported.clone(), - website: config.website.clone(), - max_fees: config.max_fees, + config, } } - pub fn set_peer_manager(&self, peer_manager: PM) { - *self.peer_manager.lock().unwrap() = Some(peer_manager); - } - - pub fn request_for_info(&self, counterparty_node_id: PublicKey, channel_id: u128) { + fn request_for_info(&self, counterparty_node_id: PublicKey, channel_id: u128) { let channel = InboundCRChannel::new(channel_id); let mut outer_state_lock = self.per_peer_state.write().unwrap(); @@ -333,40 +265,13 @@ where let mut peer_state_lock = inner_state_lock.lock().unwrap(); peer_state_lock.insert_inbound_channel(channel_id, channel); - let request_id = self.generate_request_id(); + let request_id = crate::utils::generate_request_id(&self.entropy_source); peer_state_lock.insert_request(request_id.clone(), channel_id); - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( - counterparty_node_id, - LSPS1Message::Request(request_id, LSPS1Request::GetInfo(GetInfoRequest {})).into(), - )); - } - - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } - } - - fn handle_get_info_request( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, - ) -> Result<(), LightningError> { - let response = GetInfoResponse { - supported_versions: SUPPORTED_SPEC_VERSIONS.to_vec(), - website: self.website.clone().unwrap().to_string(), - options: self - .options_config - .clone() - .ok_or(LightningError { - err: format!("Configuration for LSP server not set."), - action: ErrorAction::IgnoreAndLog(Level::Info), - }) - .unwrap(), - }; - - self.enqueue_response(*counterparty_node_id, request_id, LSPS1Response::GetInfo(response)); - Ok(()) + self.pending_messages.enqueue( + &counterparty_node_id, + LSPS1Message::Request(request_id, LSPS1Request::GetInfo(GetInfoRequest {})).into(), + ); } fn handle_get_info_response( @@ -408,7 +313,7 @@ where } }; - self.enqueue_event(Event::LSPS1(super::event::Event::GetInfoResponse { + self.pending_events.enqueue(Event::LSPS1Client(LSPS1ClientEvent::GetInfoResponse { id: channel_id, request_id, counterparty_node_id: *counterparty_node_id, @@ -430,8 +335,8 @@ where Ok(()) } - pub fn place_order( - &self, channel_id: u128, counterparty_node_id: &PublicKey, order: Order, + fn place_order( + &self, channel_id: u128, counterparty_node_id: &PublicKey, order: OrderParams, ) -> Result<(), APIError> { let outer_state_lock = self.per_peer_state.write().unwrap(); @@ -454,23 +359,17 @@ where } }; - let request_id = self.generate_request_id(); + let request_id = crate::utils::generate_request_id(&self.entropy_source); peer_state_lock.insert_request(request_id.clone(), channel_id); - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( - *counterparty_node_id, - LSPS1Message::Request( - request_id, - LSPS1Request::CreateOrder(CreateOrderRequest { order, version }), - ) - .into(), - )); - } - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } + self.pending_messages.enqueue( + counterparty_node_id, + LSPS1Message::Request( + request_id, + LSPS1Request::CreateOrder(CreateOrderRequest { order, version }), + ) + .into(), + ); } None => { return Err(APIError::APIMisuseError { @@ -481,122 +380,6 @@ where Ok(()) } - fn handle_create_order_request( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: CreateOrderRequest, - ) -> Result<(), LightningError> { - if !SUPPORTED_SPEC_VERSIONS.contains(¶ms.version) { - self.enqueue_response( - *counterparty_node_id, - request_id, - LSPS1Response::CreateOrderError(ResponseError { - code: LSPS1_CREATE_ORDER_REQUEST_INVALID_VERSION_ERROR_CODE, - message: format!("version {} is not supported", params.version), - data: Some(format!("Supported versions are {:?}", SUPPORTED_SPEC_VERSIONS)), - }), - ); - return Err(LightningError { - err: format!("client requested unsupported version {}", params.version), - action: ErrorAction::IgnoreAndLog(Level::Info), - }); - } - - if !is_valid(¶ms.order, &self.options_config.as_ref().unwrap()) { - self.enqueue_response( - *counterparty_node_id, - request_id, - LSPS1Response::CreateOrderError(ResponseError { - code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, - message: format!("Order does not match options supported by LSP server"), - data: Some(format!( - "Supported options are {:?}", - &self.options_config.as_ref().unwrap() - )), - }), - ); - return Err(LightningError { - err: format!("client requested unsupported version {}", params.version), - action: ErrorAction::IgnoreAndLog(Level::Info), - }); - } - - 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::default())); - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS1Request::CreateOrder(params.clone())); - - self.enqueue_event(Event::LSPS1(super::event::Event::CreateInvoice { - request_id, - counterparty_node_id: *counterparty_node_id, - order: params.order, - })); - - Ok(()) - } - - pub fn send_invoice_for_order( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, payment: Payment, - created_at: chrono::DateTime, expires_at: chrono::DateTime, - ) -> 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_lock = inner_state_lock.lock().unwrap(); - - match peer_state_lock.pending_requests.remove(&request_id) { - Some(LSPS1Request::CreateOrder(params)) => { - let order_id = self.generate_order_id(); - let channel = OutboundCRChannel::new( - params.order.clone(), - created_at.clone(), - expires_at.clone(), - order_id.clone(), - payment.clone(), - ); - - peer_state_lock.insert_outbound_channel(order_id.clone(), channel); - - self.enqueue_response( - *counterparty_node_id, - request_id, - LSPS1Response::CreateOrder(CreateOrderResponse { - order: params.order, - order_id, - order_state: OrderState::Created, - created_at, - expires_at, - payment, - channel: None, - }), - ); - } - - _ => { - return Err(APIError::APIMisuseError { - err: format!("No pending buy request for request_id: {:?}", request_id), - }) - } - } - } - None => { - return Err(APIError::APIMisuseError { - err: format!( - "No state for the counterparty exists: {:?}", - counterparty_node_id - ), - }) - } - } - - Ok(()) - } - fn handle_create_order_response( &self, request_id: RequestId, counterparty_node_id: &PublicKey, response: CreateOrderResponse, @@ -634,16 +417,20 @@ where } let total_fees = response.payment.fee_total_sat + response.order.client_balance_sat; - let max_fees = self.max_fees.unwrap_or(u64::MAX); - - if total_fees == response.payment.order_total_sat && total_fees < max_fees { - self.enqueue_event(Event::LSPS1(super::event::Event::DisplayOrder { - id: channel_id, - counterparty_node_id: *counterparty_node_id, - order: response.order, - payment: response.payment, - channel: response.channel, - })); + let max_channel_fees_msat = self.config.max_channel_fees_msat.unwrap_or(u64::MAX); + + if total_fees == response.payment.order_total_sat + && total_fees < max_channel_fees_msat + { + self.pending_events.enqueue(Event::LSPS1Client( + LSPS1ClientEvent::DisplayOrder { + id: channel_id, + counterparty_node_id: *counterparty_node_id, + order: response.order, + payment: response.payment, + channel: response.channel, + }, + )); } else { peer_state_lock.remove_inbound_channel(channel_id); return Err(LightningError { @@ -701,7 +488,7 @@ where } } - pub fn check_order_status( + fn check_order_status( &self, counterparty_node_id: &PublicKey, order_id: OrderId, channel_id: u128, ) -> Result<(), APIError> { let outer_state_lock = self.per_peer_state.write().unwrap(); @@ -717,25 +504,17 @@ where return Err(APIError::APIMisuseError { err: e.err }); } - let request_id = self.generate_request_id(); + let request_id = crate::utils::generate_request_id(&self.entropy_source); peer_state_lock.insert_request(request_id.clone(), channel_id); - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( - *counterparty_node_id, - LSPS1Message::Request( - request_id, - LSPS1Request::GetOrder(GetOrderRequest { - order_id: order_id.clone(), - }), - ) - .into(), - )); - } - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } + self.pending_messages.enqueue( + counterparty_node_id, + LSPS1Message::Request( + request_id, + LSPS1Request::GetOrder(GetOrderRequest { order_id: order_id.clone() }), + ) + .into(), + ); } else { return Err(APIError::APIMisuseError { err: format!("Channel with id {} not found", channel_id), @@ -752,101 +531,6 @@ where Ok(()) } - fn handle_get_order_request( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetOrderRequest, - ) -> Result<(), LightningError> { - 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_lock = inner_state_lock.lock().unwrap(); - - let outbound_channel = peer_state_lock - .outbound_channels_by_order_id - .get_mut(¶ms.order_id) - .ok_or(LightningError { - err: format!( - "Received get order request for unknown order id {:?}", - params.order_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - if let Err(e) = outbound_channel.create_payment_invoice() { - peer_state_lock.outbound_channels_by_order_id.remove(¶ms.order_id); - self.enqueue_event(Event::LSPS1(super::event::Event::Refund { - request_id, - counterparty_node_id: *counterparty_node_id, - order_id: params.order_id, - })); - return Err(e); - } - - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS1Request::GetOrder(params.clone())); - - self.enqueue_event(Event::LSPS1(super::event::Event::CheckPaymentConfirmation { - request_id, - counterparty_node_id: *counterparty_node_id, - order_id: params.order_id, - })); - } - None => { - return Err(LightningError { - err: format!("Received error response for a create order request from an unknown counterparty ({:?})",counterparty_node_id), - action: ErrorAction::IgnoreAndLog(Level::Info), - }); - } - } - - Ok(()) - } - - pub fn update_order_status( - &self, request_id: RequestId, counterparty_node_id: PublicKey, order_id: OrderId, - order_state: OrderState, channel: Option, - ) -> 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_lock = inner_state_lock.lock().unwrap(); - - if let Some(outbound_channel) = - peer_state_lock.outbound_channels_by_order_id.get_mut(&order_id) - { - let config = &outbound_channel.config; - - self.enqueue_response( - counterparty_node_id, - request_id, - LSPS1Response::GetOrder(GetOrderResponse { - response: CreateOrderResponse { - order_id, - order: config.order.clone(), - order_state, - created_at: config.created_at, - expires_at: config.expires_at, - payment: config.payment.clone(), - channel, - }, - }), - ) - } else { - return Err(APIError::APIMisuseError { - err: format!("Channel with order_id {} not found", order_id.0), - }); - } - } - None => { - return Err(APIError::APIMisuseError { - err: format!("No existing state with counterparty {}", counterparty_node_id), - }) - } - } - Ok(()) - } - fn handle_get_order_response( &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetOrderResponse, ) -> Result<(), LightningError> { @@ -923,86 +607,47 @@ where } } } - - fn enqueue_response( - &self, counterparty_node_id: PublicKey, request_id: RequestId, response: LSPS1Response, - ) { - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages - .push((counterparty_node_id, LSPS1Message::Response(request_id, response).into())); - } - - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } - } - - fn enqueue_event(&self, event: Event) { - self.pending_events.enqueue(event); - } - - fn generate_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) - } - - fn generate_request_id(&self) -> RequestId { - let bytes = self.entropy_source.get_secure_random_bytes(); - RequestId(utils::hex_str(&bytes[0..16])) - } - - fn generate_order_id(&self) -> OrderId { - let bytes = self.entropy_source.get_secure_random_bytes(); - OrderId(utils::hex_str(&bytes[0..16])) - } } -impl ProtocolMessageHandler - for CRManager +impl ProtocolMessageHandler + for LSPS1ClientHandler where ES::Target: EntropySource, CM::Target: AChannelManager, - PM::Target: APeerManager, + MQ::Target: MessageQueue, C::Target: Filter, { type ProtocolMessage = LSPS1Message; - const PROTOCOL_NUMBER: Option = Some(2); + const PROTOCOL_NUMBER: Option = Some(1); fn handle_message( &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { match message { - LSPS1Message::Request(request_id, request) => match request { - super::msgs::LSPS1Request::GetInfo(_) => { - self.handle_get_info_request(request_id, counterparty_node_id) - } - super::msgs::LSPS1Request::CreateOrder(params) => { - self.handle_create_order_request(request_id, counterparty_node_id, params) - } - super::msgs::LSPS1Request::GetOrder(params) => { - self.handle_get_order_request(request_id, counterparty_node_id, params) - } - }, LSPS1Message::Response(request_id, response) => match response { - super::msgs::LSPS1Response::GetInfo(params) => { + LSPS1Response::GetInfo(params) => { self.handle_get_info_response(request_id, counterparty_node_id, params) } - super::msgs::LSPS1Response::CreateOrder(params) => { + LSPS1Response::CreateOrder(params) => { self.handle_create_order_response(request_id, counterparty_node_id, params) } - super::msgs::LSPS1Response::CreateOrderError(params) => { + LSPS1Response::CreateOrderError(params) => { self.handle_create_order_error(request_id, counterparty_node_id, params) } - super::msgs::LSPS1Response::GetOrder(params) => { + LSPS1Response::GetOrder(params) => { self.handle_get_order_response(request_id, counterparty_node_id, params) } - super::msgs::LSPS1Response::GetOrderError(error) => { + LSPS1Response::GetOrderError(error) => { self.handle_get_order_error(request_id, counterparty_node_id, error) } }, + _ => { + debug_assert!( + false, + "Client handler received LSPS1 request message. This should never happen." + ); + Err(LightningError { err: format!("Client handler received LSPS1 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + } } } } diff --git a/src/lsps1/event.rs b/src/lsps1/event.rs index 8bfe9d9..432b928 100644 --- a/src/lsps1/event.rs +++ b/src/lsps1/event.rs @@ -1,61 +1,73 @@ -#![allow(missing_docs)] +//! Contains LSPS1 event types -use super::msgs::{ChannelInfo, OptionsSupported, Order, OrderId, Payment}; +use super::msgs::{ChannelInfo, OptionsSupported, OrderId, OrderParams, OrderPayment}; use crate::lsps0::msgs::RequestId; use crate::prelude::String; use bitcoin::secp256k1::PublicKey; -/// An "Event" which you should probably take some action in response to. +/// An event which an LSPS1 client should take some action in response to. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { +pub enum LSPS1ClientEvent { + /// TODO GetInfoResponse { + /// TODO id: u128, + /// TODO request_id: RequestId, + /// TODO counterparty_node_id: PublicKey, + /// TODO version: u16, + /// TODO website: String, + /// TODO options_supported: OptionsSupported, }, - - CreateInvoice { - request_id: RequestId, - counterparty_node_id: PublicKey, - order: Order, - }, - + /// TODO DisplayOrder { + /// TODO id: u128, + /// TODO counterparty_node_id: PublicKey, - order: Order, - payment: Payment, + /// TODO + order: OrderParams, + /// TODO + payment: OrderPayment, + /// TODO channel: Option, }, +} - PayforChannel { +/// An event which an LSPS1 server should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS1ServiceEvent { + /// TODO + CreateInvoice { + /// TODO request_id: RequestId, + /// TODO counterparty_node_id: PublicKey, - order: Order, - payment: Payment, - channel: Option, + /// TODO + order: OrderParams, }, - + /// TODO CheckPaymentConfirmation { + /// TODO request_id: RequestId, + /// TODO counterparty_node_id: PublicKey, + /// TODO order_id: OrderId, }, - - OpenChannel { - request_id: RequestId, - counterparty_node_id: PublicKey, - order_id: OrderId, - }, - + /// TODO Refund { + /// TODO request_id: RequestId, + /// TODO counterparty_node_id: PublicKey, + /// TODO order_id: OrderId, }, } diff --git a/src/lsps1/mod.rs b/src/lsps1/mod.rs index 07f75f1..06d5e75 100644 --- a/src/lsps1/mod.rs +++ b/src/lsps1/mod.rs @@ -8,8 +8,9 @@ // licenses. //! Types and primitives that implement the LSPS1: Channel Request specification. -pub(crate) mod channel_manager; -pub(crate) mod event; -/// Message, request, and other primitive types used to implement LSPS2. + +pub mod client; +pub mod event; pub mod msgs; +pub mod service; pub(crate) mod utils; diff --git a/src/lsps1/msgs.rs b/src/lsps1/msgs.rs index 535727e..c01a919 100644 --- a/src/lsps1/msgs.rs +++ b/src/lsps1/msgs.rs @@ -1,3 +1,5 @@ +//! Message, request, and other primitive types used to implement LSPS1. + use crate::lsps0::msgs::{LSPSMessage, RequestId, ResponseError}; use crate::prelude::{String, Vec}; @@ -17,135 +19,233 @@ pub(crate) const LSPS1_CREATE_ORDER_REQUEST_CLIENT_REJECTED_ERROR_CODE: i32 = 10 pub(crate) const LSPS1_CREATE_ORDER_REQUEST_INVALID_VERSION_ERROR_CODE: i32 = 1; pub(crate) const LSPS1_CREATE_ORDER_REQUEST_INVALID_TOKEN_ERROR_CODE: i32 = 2; +/// The identifier of an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct OrderId(pub String); +/// A request made to an LSP to retrieve the supported options. +/// +/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#1-lsps1info) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] #[serde(default)] pub struct GetInfoRequest {} +/// An object representing the supported protocol options. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct OptionsSupported { + /// The minimum number of block confirmations before the LSP accepts a channel as confirmed. pub minimum_channel_confirmations: u8, + /// The minimum number of block confirmations before the LSP accepts an on-chain payment as confirmed. pub minimum_onchain_payment_confirmations: u8, + /// Indicates if the LSP supports zero reserve. pub supports_zero_channel_reserve: bool, + /// Indicates the minimum amount of satoshi that is required for the LSP to accept a payment + /// on-chain. pub min_onchain_payment_size_sat: Option, + /// The maximum number of blocks a channel can be leased for. pub max_channel_expiry_blocks: u32, + /// The minimum number of satoshi that the client MUST request. pub min_initial_client_balance_sat: u64, + /// The maximum number of satoshi that the client MUST request. pub max_initial_client_balance_sat: u64, + /// The minimum number of satoshi that the LSP will provide to the channel. pub min_initial_lsp_balance_sat: u64, + /// The maximum number of satoshi that the LSP will provide to the channel. pub max_initial_lsp_balance_sat: u64, + /// The minimal channel size. pub min_channel_balance_sat: u64, + /// The maximal channel size. pub max_channel_balance_sat: u64, } +/// A response to an [`GetInfoRequest`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct GetInfoResponse { + /// A list of all supported API versions by the LSP. pub supported_versions: Vec, + /// The website of the LSP. pub website: String, + /// All options supported by the LSP. pub options: OptionsSupported, } +/// A request made to an LSP to create an order. +/// +/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#2-lsps1create_order) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CreateOrderRequest { + /// TODO: this is superfluous and should likely be removed. pub version: u16, - pub order: Order, + /// The order made. + pub order: OrderParams, } +/// An object representing an LSPS1 channel order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct Order { +pub struct OrderParams { + /// The API version that the client wants to work with. pub api_version: u16, + /// Indicates how many satoshi the LSP will provide on their side. pub lsp_balance_sat: u64, + /// Indicates how many satoshi the client will provide on their side. + /// + /// The client sends these funds to the LSP, who will push them back to the client upon opening + /// the channel. pub client_balance_sat: u64, + /// The number of blocks the client wants to wait maximally for the channel to be confirmed. pub confirms_within_blocks: u32, + /// Indicates how long the channel is leased for in block time. pub channel_expiry_blocks: u32, + /// May contain arbitrary associated data like a coupon code or a authentication token. pub token: String, - pub announce_channel: bool, + /// The address where the LSP will send the funds if the order fails. pub refund_onchain_address: Option, + /// Indicates if the channel should be announced to the network. + pub announce_channel: bool, } +/// A response to a [`CreateOrderRequest`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CreateOrderResponse { + /// The id of the channel order. pub order_id: OrderId, - pub order: Order, - pub order_state: OrderState, + /// The parameters of channel order. + pub order: OrderParams, + /// The datetime when the order was created pub created_at: chrono::DateTime, + /// The datetime when the order expires. pub expires_at: chrono::DateTime, - pub payment: Payment, + /// The current state of the order. + pub order_state: OrderState, + /// Contains details about how to pay for the order. + pub payment: OrderPayment, + /// Contains information about the channel state. pub channel: Option, } +/// An object representing the state of an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum OrderState { + /// TODO: this is superfluous and should likely be removed. Requested, + /// The order has been created. Created, + /// The LSP has opened the channel and published the funding transaction. Completed, + /// The order failed. Failed, } +/// Details regarding how to pay for an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct Payment { +pub struct OrderPayment { + /// Indicates the current state of the payment. pub state: PaymentState, + /// The total fee the LSP will charge to open this channel in satoshi. pub fee_total_sat: u64, + /// What the client needs to pay in total to open the requested channel. pub order_total_sat: u64, - pub onchain_address: String, + /// A BOLT11 invoice the client can pay to have to channel opened. pub bolt11_invoice: String, + /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel + /// opened. + pub onchain_address: String, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. pub onchain_block_confirmations_required: u8, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. pub minimum_fee_for_0conf: u8, + /// Details regarding a detected on-chain payment. pub onchain_payment: OnchainPayment, } +/// The state of an [`OrderPayment`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum PaymentState { + /// A payment is expected. ExpectPayment, + /// A Lighting payment has arrived, but the preimage has not been released yet. Hold, + /// A sufficient payment has been received. Paid, + /// The payment has been refunded. Refunded, } +/// Details regarding a detected on-chain payment. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct OnchainPayment { + /// The outpoint of the payment. pub outpoint: String, + /// The amount of satoshi paid. pub sat: u64, + /// Indicates if the LSP regards the transaction as sufficiently confirmed. pub confirmed: bool, } +/// Details regarding the state of an ordered channel. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct ChannelInfo { - pub state: ChannelStatus, + /// The current state of the channel. + pub state: ChannelState, + /// The datetime when the funding transaction has been published. pub funded_at: String, + /// The outpoint of the funding transaction. pub funding_outpoint: String, + /// The channel's short channel id. pub scid: Option, + /// The earliest datetime when the channel may be closed by the LSP. pub expires_at: String, + /// The transaction id of the channel. pub closing_transaction: Option, + /// The datetime when the closing transaction was published. pub closed_at: Option, } +/// The current state of an ordered channel. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub enum ChannelStatus { +pub enum ChannelState { + /// The funding transaction has been published. Opening, + /// The channel has been opened. Opened, + /// The channel has been closed. Closed, } +/// A request made to an LSP to retrieve information about an previously made order. +/// +/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#21-lsps1get_order) +/// for more information. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct GetOrderRequest { + /// The id of the order. pub order_id: OrderId, } +/// A response to an [`GetOrderRequest`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct GetOrderResponse { + /// The response to the order request. pub response: CreateOrderResponse, } +/// An enum that captures all the valid JSON-RPC requests in the LSPS1 protocol. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS1Request { + /// A request to learn about the options supported by the LSP. GetInfo(GetInfoRequest), + /// A request to create a channel order. CreateOrder(CreateOrderRequest), + /// A request to query a previously created channel order. GetOrder(GetOrderRequest), } impl LSPS1Request { + /// Get the JSON-RPC method name for the underlying request. pub fn method(&self) -> &str { match self { LSPS1Request::GetInfo(_) => LSPS1_GET_INFO_METHOD_NAME, @@ -155,18 +255,27 @@ impl LSPS1Request { } } +/// An enum that captures all the valid JSON-RPC responses in the LSPS1 protocol. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS1Response { + /// A successful response to a [`GetInfoRequest`]. GetInfo(GetInfoResponse), + /// A successful response to a [`CreateOrderRequest`]. CreateOrder(CreateOrderResponse), + /// An error response to a [`CreateOrderRequest`]. CreateOrderError(ResponseError), + /// A successful response to a [`GetOrderRequest`]. GetOrder(GetOrderResponse), + /// An error response to a [`GetOrderRequest`]. GetOrderError(ResponseError), } +/// An enum that captures all valid JSON-RPC messages in the LSPS1 protocol. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS1Message { + /// An LSPS1 JSON-RPC request. Request(RequestId, LSPS1Request), + /// An LSPS1 JSON-RPC response. Response(RequestId, LSPS1Response), } diff --git a/src/lsps1/service.rs b/src/lsps1/service.rs new file mode 100644 index 0000000..8211046 --- /dev/null +++ b/src/lsps1/service.rs @@ -0,0 +1,460 @@ +// This file is Copyright its original authors, visible in version contror +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS1 server object, [`LSPS1ServiceHandler`]. + +use super::event::LSPS1ServiceEvent; +use super::msgs::{ + ChannelInfo, CreateOrderRequest, CreateOrderResponse, GetInfoResponse, GetOrderRequest, + GetOrderResponse, LSPS1Message, LSPS1Request, LSPS1Response, OptionsSupported, OrderId, + OrderParams, OrderPayment, OrderState, LSPS1_CREATE_ORDER_REQUEST_INVALID_VERSION_ERROR_CODE, + LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, +}; +use super::utils::is_valid; +use crate::message_queue::MessageQueue; + +use crate::events::EventQueue; +use crate::lsps0::msgs::{ProtocolMessageHandler, RequestId}; +use crate::prelude::{HashMap, String, ToString}; +use crate::sync::{Arc, Mutex, RwLock}; +use crate::utils; +use crate::{events::Event, lsps0::msgs::ResponseError}; + +use lightning::chain::Filter; +use lightning::ln::channelmanager::AChannelManager; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::errors::APIError; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use chrono::Utc; +use core::ops::Deref; + +const SUPPORTED_SPEC_VERSIONS: [u16; 1] = [1]; + +/// Server-side configuration options for LSPS1 channel requests. +#[derive(Clone, Debug)] +pub struct LSPS1ServiceConfig { + /// A token to be send with each channel request. + pub token: Option, + /// The options supported by the LSP. + pub options_supported: Option, + /// The LSP's website. + pub website: Option, +} + +struct ChannelStateError(String); + +impl From for LightningError { + fn from(value: ChannelStateError) -> Self { + LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } + } +} + +#[derive(PartialEq, Debug)] +enum OutboundRequestState { + OrderCreated { order_id: OrderId }, + WaitingPayment { order_id: OrderId }, + Ready, +} + +impl OutboundRequestState { + fn create_payment_invoice(&self) -> Result { + match self { + OutboundRequestState::OrderCreated { order_id } => { + Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) + } + state => Err(ChannelStateError(format!( + "Received unexpected get_versions response. JIT Channel was in state: {:?}", + state + ))), + } + } +} + +struct OutboundLSPS1Config { + order: OrderParams, + created_at: chrono::DateTime, + expires_at: chrono::DateTime, + payment: OrderPayment, +} + +struct OutboundCRChannel { + state: OutboundRequestState, + config: OutboundLSPS1Config, +} + +impl OutboundCRChannel { + fn new( + order: OrderParams, created_at: chrono::DateTime, expires_at: chrono::DateTime, + order_id: OrderId, payment: OrderPayment, + ) -> Self { + Self { + state: OutboundRequestState::OrderCreated { order_id }, + config: OutboundLSPS1Config { order, created_at, expires_at, payment }, + } + } + fn create_payment_invoice(&mut self) -> Result<(), LightningError> { + self.state = self.state.create_payment_invoice()?; + Ok(()) + } + + fn check_order_validity(&self, options_supported: &OptionsSupported) -> bool { + let order = &self.config.order; + + is_valid(order, options_supported) + } +} + +#[derive(Default)] +struct PeerState { + outbound_channels_by_order_id: HashMap, + request_to_cid: HashMap, + pending_requests: HashMap, +} + +impl PeerState { + fn insert_outbound_channel(&mut self, order_id: OrderId, channel: OutboundCRChannel) { + self.outbound_channels_by_order_id.insert(order_id, channel); + } + + fn insert_request(&mut self, request_id: RequestId, channel_id: u128) { + self.request_to_cid.insert(request_id, channel_id); + } + + fn remove_outbound_channel(&mut self, order_id: OrderId) { + self.outbound_channels_by_order_id.remove(&order_id); + } +} + +/// The main object allowing to send and receive LSPS1 messages. +pub struct LSPS1ServiceHandler +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + MQ::Target: MessageQueue, + C::Target: Filter, +{ + entropy_source: ES, + channel_manager: CM, + chain_source: Option, + pending_messages: MQ, + pending_events: Arc, + per_peer_state: RwLock>>, + config: LSPS1ServiceConfig, +} + +impl LSPS1ServiceHandler +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + MQ::Target: MessageQueue, + C::Target: Filter, + ES::Target: EntropySource, +{ + pub(crate) fn new( + entropy_source: ES, pending_messages: MQ, pending_events: Arc, + channel_manager: CM, chain_source: Option, config: LSPS1ServiceConfig, + ) -> Self { + Self { + entropy_source, + channel_manager, + chain_source, + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + config, + } + } + + fn handle_get_info_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + let response = GetInfoResponse { + supported_versions: SUPPORTED_SPEC_VERSIONS.to_vec(), + website: self.config.website.clone().unwrap().to_string(), + options: self + .config + .options_supported + .clone() + .ok_or(LightningError { + err: format!("Configuration for LSP server not set."), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + .unwrap(), + }; + + self.enqueue_response(counterparty_node_id, request_id, LSPS1Response::GetInfo(response)); + Ok(()) + } + + fn handle_create_order_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: CreateOrderRequest, + ) -> Result<(), LightningError> { + if !SUPPORTED_SPEC_VERSIONS.contains(¶ms.version) { + self.enqueue_response( + counterparty_node_id, + request_id, + LSPS1Response::CreateOrderError(ResponseError { + code: LSPS1_CREATE_ORDER_REQUEST_INVALID_VERSION_ERROR_CODE, + message: format!("version {} is not supported", params.version), + data: Some(format!("Supported versions are {:?}", SUPPORTED_SPEC_VERSIONS)), + }), + ); + return Err(LightningError { + err: format!("client requested unsupported version {}", params.version), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + if !is_valid(¶ms.order, &self.config.options_supported.as_ref().unwrap()) { + self.enqueue_response( + counterparty_node_id, + request_id, + LSPS1Response::CreateOrderError(ResponseError { + code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, + message: format!("Order does not match options supported by LSP server"), + data: Some(format!( + "Supported options are {:?}", + &self.config.options_supported.as_ref().unwrap() + )), + }), + ); + return Err(LightningError { + err: format!("client requested unsupported version {}", params.version), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + 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::default())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS1Request::CreateOrder(params.clone())); + + self.pending_events.enqueue(Event::LSPS1Service(LSPS1ServiceEvent::CreateInvoice { + request_id, + counterparty_node_id: *counterparty_node_id, + order: params.order, + })); + + Ok(()) + } + + fn send_invoice_for_order( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, payment: OrderPayment, + created_at: chrono::DateTime, expires_at: chrono::DateTime, + ) -> 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_lock = inner_state_lock.lock().unwrap(); + + match peer_state_lock.pending_requests.remove(&request_id) { + Some(LSPS1Request::CreateOrder(params)) => { + let order_id = self.generate_order_id(); + let channel = OutboundCRChannel::new( + params.order.clone(), + created_at.clone(), + expires_at.clone(), + order_id.clone(), + payment.clone(), + ); + + peer_state_lock.insert_outbound_channel(order_id.clone(), channel); + + self.enqueue_response( + counterparty_node_id, + request_id, + LSPS1Response::CreateOrder(CreateOrderResponse { + order: params.order, + order_id, + order_state: OrderState::Created, + created_at, + expires_at, + payment, + channel: None, + }), + ); + } + + _ => { + return Err(APIError::APIMisuseError { + err: format!("No pending buy request for request_id: {:?}", request_id), + }) + } + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!( + "No state for the counterparty exists: {:?}", + counterparty_node_id + ), + }) + } + } + + Ok(()) + } + + fn handle_get_order_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetOrderRequest, + ) -> Result<(), LightningError> { + 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_lock = inner_state_lock.lock().unwrap(); + + let outbound_channel = peer_state_lock + .outbound_channels_by_order_id + .get_mut(¶ms.order_id) + .ok_or(LightningError { + err: format!( + "Received get order request for unknown order id {:?}", + params.order_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + if let Err(e) = outbound_channel.create_payment_invoice() { + peer_state_lock.outbound_channels_by_order_id.remove(¶ms.order_id); + self.pending_events.enqueue(Event::LSPS1Service(LSPS1ServiceEvent::Refund { + request_id, + counterparty_node_id: *counterparty_node_id, + order_id: params.order_id, + })); + return Err(e); + } + + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS1Request::GetOrder(params.clone())); + + self.pending_events.enqueue(Event::LSPS1Service( + LSPS1ServiceEvent::CheckPaymentConfirmation { + request_id, + counterparty_node_id: *counterparty_node_id, + order_id: params.order_id, + }, + )); + } + None => { + return Err(LightningError { + err: format!("Received error response for a create order request from an unknown counterparty ({:?})",counterparty_node_id), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + } + + Ok(()) + } + + fn update_order_status( + &self, request_id: RequestId, counterparty_node_id: PublicKey, order_id: OrderId, + order_state: OrderState, channel: Option, + ) -> 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_lock = inner_state_lock.lock().unwrap(); + + if let Some(outbound_channel) = + peer_state_lock.outbound_channels_by_order_id.get_mut(&order_id) + { + let config = &outbound_channel.config; + + self.enqueue_response( + &counterparty_node_id, + request_id, + LSPS1Response::GetOrder(GetOrderResponse { + response: CreateOrderResponse { + order_id, + order: config.order.clone(), + order_state, + created_at: config.created_at, + expires_at: config.expires_at, + payment: config.payment.clone(), + channel, + }, + }), + ) + } else { + return Err(APIError::APIMisuseError { + err: format!("Channel with order_id {} not found", order_id.0), + }); + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!("No existing state with counterparty {}", counterparty_node_id), + }) + } + } + Ok(()) + } + + fn enqueue_response( + &self, counterparty_node_id: &PublicKey, request_id: RequestId, response: LSPS1Response, + ) { + self.pending_messages + .enqueue(counterparty_node_id, LSPS1Message::Response(request_id, response).into()); + } + + fn generate_order_id(&self) -> OrderId { + let bytes = self.entropy_source.get_secure_random_bytes(); + OrderId(utils::hex_str(&bytes[0..16])) + } +} + +impl ProtocolMessageHandler + for LSPS1ServiceHandler +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + MQ::Target: MessageQueue, + C::Target: Filter, +{ + type ProtocolMessage = LSPS1Message; + const PROTOCOL_NUMBER: Option = Some(1); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS1Message::Request(request_id, request) => match request { + LSPS1Request::GetInfo(_) => { + self.handle_get_info_request(request_id, counterparty_node_id) + } + LSPS1Request::CreateOrder(params) => { + self.handle_create_order_request(request_id, counterparty_node_id, params) + } + LSPS1Request::GetOrder(params) => { + self.handle_get_order_request(request_id, counterparty_node_id, params) + } + }, + _ => { + debug_assert!( + false, + "Service handler received LSPS1 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS1 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + } + } + } +} diff --git a/src/lsps1/utils.rs b/src/lsps1/utils.rs index 298b6d4..7a67521 100644 --- a/src/lsps1/utils.rs +++ b/src/lsps1/utils.rs @@ -1,10 +1,10 @@ -use super::msgs::{OptionsSupported, Order}; +use super::msgs::{OptionsSupported, OrderParams}; pub fn check_range(min: u64, max: u64, value: u64) -> bool { (value >= min) && (value <= max) } -pub fn is_valid(order: &Order, options: &OptionsSupported) -> bool { +pub fn is_valid(order: &OrderParams, options: &OptionsSupported) -> bool { let bool = check_range( options.min_initial_client_balance_sat, options.max_initial_client_balance_sat, diff --git a/src/lsps2/client.rs b/src/lsps2/client.rs new file mode 100644 index 0000000..dfb0be4 --- /dev/null +++ b/src/lsps2/client.rs @@ -0,0 +1,636 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS2 client object, [`LSPS2ClientHandler`]. + +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, Vec}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::errors::APIError; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +use crate::lsps2::msgs::{ + BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, GetVersionsRequest, + GetVersionsResponse, JITChannelScid, LSPS2Message, LSPS2Request, LSPS2Response, + OpeningFeeParams, +}; + +/// Client-side configuration options for JIT channels. +#[derive(Clone, Debug, Copy)] +pub struct LSPS2ClientConfig {} + +const SUPPORTED_SPEC_VERSIONS: [u16; 1] = [1]; + +struct ChannelStateError(String); + +impl From for LightningError { + fn from(value: ChannelStateError) -> Self { + LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } + } +} + +struct InboundJITChannelConfig { + pub user_id: u128, + pub token: Option, + pub payment_size_msat: Option, +} + +#[derive(PartialEq, Debug)] +enum InboundJITChannelState { + VersionsRequested, + MenuRequested { version: u16 }, + PendingMenuSelection { version: u16 }, + BuyRequested { version: u16 }, + PendingPayment { client_trusts_lsp: bool, short_channel_id: JITChannelScid }, +} + +impl InboundJITChannelState { + fn versions_received(&self, versions: Vec) -> Result { + let max_shared_version = versions + .iter() + .filter(|version| SUPPORTED_SPEC_VERSIONS.contains(version)) + .max() + .cloned() + .ok_or(ChannelStateError(format!( + "LSP does not support any of our specification versions. ours = {:?}. theirs = {:?}", + SUPPORTED_SPEC_VERSIONS, versions + )))?; + + match self { + InboundJITChannelState::VersionsRequested => { + Ok(InboundJITChannelState::MenuRequested { version: max_shared_version }) + } + state => Err(ChannelStateError(format!( + "Received unexpected get_versions response. JIT Channel was in state: {:?}", + state + ))), + } + } + + fn info_received(&self) -> Result { + match self { + InboundJITChannelState::MenuRequested { version } => { + Ok(InboundJITChannelState::PendingMenuSelection { version: *version }) + } + state => Err(ChannelStateError(format!( + "Received unexpected get_info response. JIT Channel was in state: {:?}", + state + ))), + } + } + + fn opening_fee_params_selected(&self) -> Result { + match self { + InboundJITChannelState::PendingMenuSelection { version } => { + Ok(InboundJITChannelState::BuyRequested { version: *version }) + } + state => Err(ChannelStateError(format!( + "Opening fee params selected when JIT Channel was in state: {:?}", + state + ))), + } + } + + fn invoice_params_received( + &self, client_trusts_lsp: bool, short_channel_id: JITChannelScid, + ) -> Result { + match self { + InboundJITChannelState::BuyRequested { .. } => { + Ok(InboundJITChannelState::PendingPayment { client_trusts_lsp, short_channel_id }) + } + state => Err(ChannelStateError(format!( + "Invoice params received when JIT Channel was in state: {:?}", + state + ))), + } + } +} + +struct InboundJITChannel { + id: u128, + state: InboundJITChannelState, + config: InboundJITChannelConfig, +} + +impl InboundJITChannel { + fn new(id: u128, user_id: u128, payment_size_msat: Option, token: Option) -> Self { + Self { + id, + config: InboundJITChannelConfig { user_id, payment_size_msat, token }, + state: InboundJITChannelState::VersionsRequested, + } + } + + fn versions_received(&mut self, versions: Vec) -> Result { + self.state = self.state.versions_received(versions)?; + + match self.state { + InboundJITChannelState::MenuRequested { version } => Ok(version), + _ => Err(LightningError { + action: ErrorAction::IgnoreAndLog(Level::Error), + err: "impossible state transition".to_string(), + }), + } + } + + fn info_received(&mut self) -> Result<(), LightningError> { + self.state = self.state.info_received()?; + Ok(()) + } + + fn opening_fee_params_selected(&mut self) -> Result { + self.state = self.state.opening_fee_params_selected()?; + + match self.state { + InboundJITChannelState::BuyRequested { version } => Ok(version), + _ => Err(LightningError { + action: ErrorAction::IgnoreAndLog(Level::Error), + err: "impossible state transition".to_string(), + }), + } + } + + fn invoice_params_received( + &mut self, client_trusts_lsp: bool, jit_channel_scid: JITChannelScid, + ) -> Result<(), LightningError> { + self.state = self.state.invoice_params_received(client_trusts_lsp, jit_channel_scid)?; + Ok(()) + } +} + +struct PeerState { + inbound_channels_by_id: HashMap, + request_to_cid: HashMap, +} + +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); + } +} + +/// The main object allowing to send and receive LSPS2 messages. +pub struct LSPS2ClientHandler +where + ES::Target: EntropySource, + MQ::Target: MessageQueue, +{ + entropy_source: ES, + pending_messages: MQ, + pending_events: Arc, + per_peer_state: RwLock>>, + _config: LSPS2ClientConfig, +} + +impl LSPS2ClientHandler +where + ES::Target: EntropySource, + MQ::Target: MessageQueue, +{ + /// Constructs an `LSPS2ClientHandler`. + pub(crate) fn new( + entropy_source: ES, pending_messages: MQ, pending_events: Arc, + config: LSPS2ClientConfig, + ) -> Self { + Self { + entropy_source, + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + _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. + /// + /// `counterparty_node_id` is the node_id of the LSP you would like to use. + /// + /// 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. + /// + /// `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, + token: Option, user_channel_id: u128, + ) { + let jit_channel_id = self.generate_jit_channel_id(); + let channel = + InboundJITChannel::new(jit_channel_id, user_channel_id, payment_size_msat, token); + + 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); + + let request_id = crate::utils::generate_request_id(&self.entropy_source); + peer_state_lock.insert_request(request_id.clone(), jit_channel_id); + + self.pending_messages.enqueue( + &counterparty_node_id, + LSPS2Message::Request(request_id, LSPS2Request::GetVersions(GetVersionsRequest {})) + .into(), + ); + } + + /// Used by client to confirm which channel parameters to use for the JIT Channel buy request. + /// 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, + ) -> 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) + { + let version = match jit_channel.opening_fee_params_selected() { + Ok(version) => version, + 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 { + version, + 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), + }) + } + } + + Ok(()) + } + + 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) + } + + fn handle_get_versions_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetVersionsResponse, + ) -> Result<(), LightningError> { + 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!( + "Received get_versions 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_versions response for an unknown channel: {:?}", + jit_channel_id, + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let token = jit_channel.config.token.clone(); + + let version = match jit_channel.versions_received(result.versions) { + Ok(version) => version, + Err(e) => { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(e); + } + }; + + let request_id = crate::utils::generate_request_id(&self.entropy_source); + peer_state.insert_request(request_id.clone(), jit_channel_id); + + self.pending_messages.enqueue( + counterparty_node_id, + LSPS2Message::Request( + request_id, + LSPS2Request::GetInfo(GetInfoRequest { version, token }), + ) + .into(), + ); + } + None => { + return Err(LightningError { + err: format!( + "Received get_versions response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + } + } + + Ok(()) + } + + fn handle_get_info_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse, + ) -> Result<(), LightningError> { + 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(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 { + 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, + }, + )); + } + None => { + return Err(LightningError { + err: format!( + "Received get_info response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + } + } + + Ok(()) + } + + fn handle_get_info_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError, + ) -> Result<(), LightningError> { + 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(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 => { + return Err(LightningError { err: format!("Received error response for a get_info request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + } + } + + fn handle_buy_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: BuyResponse, + ) -> Result<(), LightningError> { + 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!( + "Received buy 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 buy response for an unknown channel: {:?}", + jit_channel_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + if let Err(e) = jit_channel.invoice_params_received( + result.client_trusts_lsp, + result.jit_channel_scid.clone(), + ) { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(e); + } + + if let Ok(scid) = result.jit_channel_scid.to_scid() { + self.pending_events.enqueue(Event::LSPS2Client( + LSPS2ClientEvent::InvoiceGenerationReady { + counterparty_node_id: *counterparty_node_id, + 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, + }, + )); + } else { + return Err(LightningError { + err: format!( + "Received buy response with an invalid scid {:?}", + result.jit_channel_scid + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + } + None => { + return Err(LightningError { + err: format!( + "Received buy response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + } + Ok(()) + } + + fn handle_buy_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError, + ) -> Result<(), LightningError> { + 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(); + + let jit_channel_id = + peer_state.request_to_cid.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 { + err: format!( + "Received buy error for an unknown channel: {:?}", + jit_channel_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + Ok(()) + } + None => { + return Err(LightningError { err: format!("Received error response for a buy request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + } + } +} + +impl ProtocolMessageHandler for LSPS2ClientHandler +where + ES::Target: EntropySource, + MQ::Target: MessageQueue, +{ + type ProtocolMessage = LSPS2Message; + const PROTOCOL_NUMBER: Option = Some(2); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS2Message::Response(request_id, response) => match response { + LSPS2Response::GetVersions(result) => { + self.handle_get_versions_response(request_id, counterparty_node_id, result) + } + LSPS2Response::GetInfo(result) => { + self.handle_get_info_response(request_id, counterparty_node_id, result) + } + LSPS2Response::GetInfoError(error) => { + self.handle_get_info_error(request_id, counterparty_node_id, error) + } + LSPS2Response::Buy(result) => { + self.handle_buy_response(request_id, counterparty_node_id, result) + } + LSPS2Response::BuyError(error) => { + self.handle_buy_error(request_id, counterparty_node_id, error) + } + }, + _ => { + debug_assert!( + false, + "Client handler received LSPS2 request message. This should never happen." + ); + Err(LightningError { err: format!("Client handler received LSPS2 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + } + } + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/lsps2/event.rs b/src/lsps2/event.rs index 6f094ce..367dfab 100644 --- a/src/lsps2/event.rs +++ b/src/lsps2/event.rs @@ -7,49 +7,29 @@ // You may not use this file except in accordance with one or both of these // licenses. +//! Contains LSPS2 event types + use super::msgs::OpeningFeeParams; use crate::lsps0::msgs::RequestId; use crate::prelude::{String, Vec}; use bitcoin::secp256k1::PublicKey; -/// An event which you should probably take some action in response to. +/// An event which an LSPS2 client should take some action in response to. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum LSPS2Event { - /// A request from a client for information about JIT Channel parameters. - /// - /// You must calculate the parameters for this client and pass them to - /// [`LiquidityManager::opening_fee_params_generated`]. - /// - /// If an unrecognized or stale token is provided you can use - /// `[LiquidityManager::invalid_token_provided`] to error the request. - /// - /// [`LiquidityManager::opening_fee_params_generated`]: crate::LiquidityManager::opening_fee_params_generated - /// [`LiquidityManager::invalid_token_provided`]: crate::LiquidityManager::invalid_token_provided - GetInfo { - /// An identifier that must be passed to [`LiquidityManager::opening_fee_params_generated`]. - /// - /// [`LiquidityManager::opening_fee_params_generated`]: crate::LiquidityManager::opening_fee_params_generated - request_id: RequestId, - /// The node id of the client making the information request. - counterparty_node_id: PublicKey, - /// The protocol version they would like to use. - version: u16, - /// An optional token that can be used as an API key, coupon code, etc. - token: Option, - }, +pub enum LSPS2ClientEvent { /// Information from the LSP about their current fee rates and channel parameters. /// - /// You must call [`LiquidityManager::opening_fee_params_selected`] with the fee parameter + /// You must call [`LSPS2ClientHandler::opening_fee_params_selected`] with the fee parameter /// you want to use if you wish to proceed opening a channel. /// - /// [`LiquidityManager::opening_fee_params_selected`]: crate::LiquidityManager::opening_fee_params_selected + /// [`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 [`LiquidityManager::opening_fee_params_selected`]. + /// It needs to be passed to [`LSPS2ClientHandler::opening_fee_params_selected`]. /// - /// [`LiquidityManager::opening_fee_params_selected`]: crate::LiquidityManager::opening_fee_params_selected + /// [`LSPS2ClientHandler::opening_fee_params_selected`]: crate::lsps2::client::LSPS2ClientHandler::opening_fee_params_selected jit_channel_id: u128, /// The node id of the LSP that provided this response. counterparty_node_id: PublicKey, @@ -60,11 +40,58 @@ pub enum LSPS2Event { 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 [`LiquidityManager::lsps2_create_invoice`]. + /// 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. + /// + /// When the invoice is paid the LSP will open a channel to you + /// with the previously agreed upon parameters. + InvoiceGenerationReady { + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The short channel id to use in the route hint. + scid: u64, + /// The `cltv_expiry_delta` to use in the route hint. + cltv_expiry_delta: u32, + /// The initial payment size you specified. + payment_size_msat: Option, + /// The trust model the LSP expects. + client_trusts_lsp: bool, + /// The `user_channel_id` value passed in to [`LSPS2ClientHandler::create_invoice`]. /// - /// [`LiquidityManager::lsps2_create_invoice`]: crate::LiquidityManager::lsps2_create_invoice + /// [`LSPS2ClientHandler::create_invoice`]: crate::lsps2::client::LSPS2ClientHandler::create_invoice user_channel_id: u128, }, +} + +/// An event which an LSPS2 server should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS2ServiceEvent { + /// A request from a client for information about JIT Channel parameters. + /// + /// You must calculate the parameters for this client and pass them to + /// [`LSPS2ServiceHandler::opening_fee_params_generated`]. + /// + /// If an unrecognized or stale token is provided you can use + /// `[LSPS2ServiceHandler::invalid_token_provided`] to error the request. + /// + /// [`LSPS2ServiceHandler::opening_fee_params_generated`]: crate::lsps2::service::LSPS2ServiceHandler::opening_fee_params_generated + /// [`LSPS2ServiceHandler::invalid_token_provided`]: crate::lsps2::service::LSPS2ServiceHandler::invalid_token_provided + GetInfo { + /// An identifier that must be passed to [`LSPS2ServiceHandler::opening_fee_params_generated`]. + /// + /// [`LSPS2ServiceHandler::opening_fee_params_generated`]: crate::lsps2::service::LSPS2ServiceHandler::opening_fee_params_generated + request_id: RequestId, + /// The node id of the client making the information request. + counterparty_node_id: PublicKey, + /// The protocol version they would like to use. + version: u16, + /// An optional token that can be used as an API key, coupon code, etc. + token: Option, + }, /// A client has selected a opening fee parameter to use and would like to /// purchase a channel with an optional initial payment size. /// @@ -72,13 +99,13 @@ pub enum LSPS2Event { /// If `payment_size_msat` is [`Option::None`] then the payer cannot use MPP. /// /// You must generate an scid and `cltv_expiry_delta` for them to use - /// and call [`LiquidityManager::invoice_parameters_generated`]. + /// and call [`LSPS2ServiceHandler::invoice_parameters_generated`]. /// - /// [`LiquidityManager::invoice_parameters_generated`]: crate::LiquidityManager::invoice_parameters_generated + /// [`LSPS2ServiceHandler::invoice_parameters_generated`]: crate::lsps2::service::LSPS2ServiceHandler::invoice_parameters_generated BuyRequest { - /// An identifier that must be passed into [`LiquidityManager::invoice_parameters_generated`]. + /// An identifier that must be passed into [`LSPS2ServiceHandler::invoice_parameters_generated`]. /// - /// [`LiquidityManager::invoice_parameters_generated`]: crate::LiquidityManager::invoice_parameters_generated + /// [`LSPS2ServiceHandler::invoice_parameters_generated`]: crate::lsps2::service::LSPS2ServiceHandler::invoice_parameters_generated request_id: RequestId, /// The client node id that is making this request. counterparty_node_id: PublicKey, @@ -89,26 +116,6 @@ pub enum LSPS2Event { /// The size of the initial payment they would like to receive. payment_size_msat: Option, }, - /// Use the provided fields to generate an invoice and give to payer. - /// - /// When the invoice is paid the LSP will open a channel to you - /// with the previously agreed upon parameters. - InvoiceGenerationReady { - /// The node id of the LSP. - counterparty_node_id: PublicKey, - /// The short channel id to use in the route hint. - scid: u64, - /// The `cltv_expiry_delta` to use in the route hint. - cltv_expiry_delta: u32, - /// The initial payment size you specified. - payment_size_msat: Option, - /// The trust model the LSP expects. - client_trusts_lsp: bool, - /// The `user_channel_id` value passed in to [`LiquidityManager::lsps2_create_invoice`]. - /// - /// [`LiquidityManager::lsps2_create_invoice`]: crate::LiquidityManager::lsps2_create_invoice - user_channel_id: u128, - }, /// You should open a channel using [`ChannelManager::create_channel`]. /// /// [`ChannelManager::create_channel`]: lightning::ln::channelmanager::ChannelManager::create_channel diff --git a/src/lsps2/mod.rs b/src/lsps2/mod.rs index 2cee68b..1ba8173 100644 --- a/src/lsps2/mod.rs +++ b/src/lsps2/mod.rs @@ -9,11 +9,8 @@ //! Implementation of LSPS2: JIT Channel Negotiation specification. -pub(crate) mod channel_manager; -pub(crate) mod event; -/// Message, request, and other primitive types used to implement LSPS2. +pub mod client; +pub mod event; pub mod msgs; +pub mod service; pub(crate) mod utils; - -pub use event::LSPS2Event; -pub use msgs::{BuyResponse, GetInfoResponse, OpeningFeeParams, RawOpeningFeeParams}; diff --git a/src/lsps2/msgs.rs b/src/lsps2/msgs.rs index d1b9205..5aac1ab 100644 --- a/src/lsps2/msgs.rs +++ b/src/lsps2/msgs.rs @@ -1,3 +1,5 @@ +//! Message, request, and other primitive types used to implement LSPS2. + use core::convert::TryFrom; use bitcoin::hashes::hmac::{Hmac, HmacEngine}; @@ -126,9 +128,9 @@ pub struct BuyRequest { /// A newtype that holds a `short_channel_id` in human readable format of BBBxTTTx000. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct JitChannelScid(String); +pub struct JITChannelScid(String); -impl From for JitChannelScid { +impl From for JITChannelScid { fn from(scid: u64) -> Self { let block = utils::block_from_scid(&scid); let tx_index = utils::tx_index_from_scid(&scid); @@ -138,8 +140,8 @@ impl From for JitChannelScid { } } -impl JitChannelScid { - /// Try to convert a [`JitChannelScid`] into a u64 used by LDK. +impl JITChannelScid { + /// Try to convert a [`JITChannelScid`] into a u64 used by LDK. pub fn to_scid(&self) -> Result { utils::scid_from_human_readable_string(&self.0) } @@ -151,7 +153,7 @@ impl JitChannelScid { #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct BuyResponse { /// The short channel id used by LSP to identify need to open channel. - pub jit_channel_scid: JitChannelScid, + pub jit_channel_scid: JITChannelScid, /// The locktime expiry delta the lsp requires. pub lsp_cltv_expiry_delta: u32, /// A flag that indicates who is trusting who. diff --git a/src/lsps2/channel_manager.rs b/src/lsps2/service.rs similarity index 51% rename from src/lsps2/channel_manager.rs rename to src/lsps2/service.rs index 9dac867..706d8ab 100644 --- a/src/lsps2/channel_manager.rs +++ b/src/lsps2/service.rs @@ -7,21 +7,20 @@ // You may not use this file except in accordance with one or both of these // licenses. +//! Contains the main LSPS2 server-side object, [`LSPS2ServiceHandler`]. + use crate::events::EventQueue; -use crate::lsps0::message_handler::ProtocolMessageHandler; -use crate::lsps0::msgs::{LSPSMessage, RequestId}; +use crate::lsps0::msgs::{ProtocolMessageHandler, RequestId}; +use crate::lsps2::event::LSPS2ServiceEvent; use crate::lsps2::utils::{compute_opening_fee, is_valid_opening_fee_params}; -use crate::lsps2::LSPS2Event; +use crate::message_queue::MessageQueue; use crate::prelude::{HashMap, String, ToString, Vec}; use crate::sync::{Arc, Mutex, RwLock}; use crate::{events::Event, lsps0::msgs::ResponseError}; -use crate::{utils, JITChannelsConfig}; use lightning::ln::channelmanager::{AChannelManager, InterceptId}; use lightning::ln::msgs::{ErrorAction, LightningError}; -use lightning::ln::peer_handler::APeerManager; use lightning::ln::ChannelId; -use lightning::sign::EntropySource; use lightning::util::errors::APIError; use lightning::util::logger::Level; @@ -31,9 +30,9 @@ use core::convert::TryInto; use core::ops::Deref; use crate::lsps2::msgs::{ - BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, GetVersionsRequest, - GetVersionsResponse, JitChannelScid, LSPS2Message, LSPS2Request, LSPS2Response, - OpeningFeeParams, RawOpeningFeeParams, LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, + BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, GetVersionsResponse, LSPS2Message, + LSPS2Request, LSPS2Response, OpeningFeeParams, RawOpeningFeeParams, + LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, LSPS2_BUY_REQUEST_INVALID_VERSION_ERROR_CODE, LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, @@ -41,6 +40,19 @@ use crate::lsps2::msgs::{ LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, }; +/// Server-side configuration options for JIT channels. +#[derive(Clone, Debug)] +pub struct LSPS2ServiceConfig { + /// Used to calculate the promise for channel parameters supplied to clients. + /// + /// Note: If this changes then old promises given out will be considered invalid. + pub promise_secret: [u8; 32], + /// The minimum payment size you are willing to accept. + pub min_payment_size_msat: u64, + /// The maximum payment size you are willing to accept. + pub max_payment_size_msat: u64, +} + const SUPPORTED_SPEC_VERSIONS: [u16; 1] = [1]; #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -57,137 +69,6 @@ impl From for LightningError { } } -struct InboundJITChannelConfig { - pub user_id: u128, - pub token: Option, - pub payment_size_msat: Option, -} - -#[derive(PartialEq, Debug)] -enum InboundJITChannelState { - VersionsRequested, - MenuRequested { version: u16 }, - PendingMenuSelection { version: u16 }, - BuyRequested { version: u16 }, - PendingPayment { client_trusts_lsp: bool, short_channel_id: JitChannelScid }, -} - -impl InboundJITChannelState { - fn versions_received(&self, versions: Vec) -> Result { - let max_shared_version = versions - .iter() - .filter(|version| SUPPORTED_SPEC_VERSIONS.contains(version)) - .max() - .cloned() - .ok_or(ChannelStateError(format!( - "LSP does not support any of our specification versions. ours = {:?}. theirs = {:?}", - SUPPORTED_SPEC_VERSIONS, versions - )))?; - - match self { - InboundJITChannelState::VersionsRequested => { - Ok(InboundJITChannelState::MenuRequested { version: max_shared_version }) - } - state => Err(ChannelStateError(format!( - "Received unexpected get_versions response. JIT Channel was in state: {:?}", - state - ))), - } - } - - fn info_received(&self) -> Result { - match self { - InboundJITChannelState::MenuRequested { version } => { - Ok(InboundJITChannelState::PendingMenuSelection { version: *version }) - } - state => Err(ChannelStateError(format!( - "Received unexpected get_info response. JIT Channel was in state: {:?}", - state - ))), - } - } - - fn opening_fee_params_selected(&self) -> Result { - match self { - InboundJITChannelState::PendingMenuSelection { version } => { - Ok(InboundJITChannelState::BuyRequested { version: *version }) - } - state => Err(ChannelStateError(format!( - "Opening fee params selected when JIT Channel was in state: {:?}", - state - ))), - } - } - - fn invoice_params_received( - &self, client_trusts_lsp: bool, short_channel_id: JitChannelScid, - ) -> Result { - match self { - InboundJITChannelState::BuyRequested { .. } => { - Ok(InboundJITChannelState::PendingPayment { client_trusts_lsp, short_channel_id }) - } - state => Err(ChannelStateError(format!( - "Invoice params received when JIT Channel was in state: {:?}", - state - ))), - } - } -} - -struct InboundJITChannel { - id: u128, - state: InboundJITChannelState, - config: InboundJITChannelConfig, -} - -impl InboundJITChannel { - pub fn new( - id: u128, user_id: u128, payment_size_msat: Option, token: Option, - ) -> Self { - Self { - id, - config: InboundJITChannelConfig { user_id, payment_size_msat, token }, - state: InboundJITChannelState::VersionsRequested, - } - } - - pub fn versions_received(&mut self, versions: Vec) -> Result { - self.state = self.state.versions_received(versions)?; - - match self.state { - InboundJITChannelState::MenuRequested { version } => Ok(version), - _ => Err(LightningError { - action: ErrorAction::IgnoreAndLog(Level::Error), - err: "impossible state transition".to_string(), - }), - } - } - - pub fn info_received(&mut self) -> Result<(), LightningError> { - self.state = self.state.info_received()?; - Ok(()) - } - - pub fn opening_fee_params_selected(&mut self) -> Result { - self.state = self.state.opening_fee_params_selected()?; - - match self.state { - InboundJITChannelState::BuyRequested { version } => Ok(version), - _ => Err(LightningError { - action: ErrorAction::IgnoreAndLog(Level::Error), - err: "impossible state transition".to_string(), - }), - } - } - - pub fn invoice_params_received( - &mut self, client_trusts_lsp: bool, jit_channel_scid: JitChannelScid, - ) -> Result<(), LightningError> { - self.state = self.state.invoice_params_received(client_trusts_lsp, jit_channel_scid)?; - Ok(()) - } -} - #[derive(PartialEq, Debug)] enum OutboundJITChannelState { AwaitingPayment { @@ -208,7 +89,7 @@ enum OutboundJITChannelState { } impl OutboundJITChannelState { - pub fn new(payment_size_msat: Option, opening_fee_params: OpeningFeeParams) -> Self { + fn new(payment_size_msat: Option, opening_fee_params: OpeningFeeParams) -> Self { OutboundJITChannelState::AwaitingPayment { min_fee_msat: opening_fee_params.min_fee_msat, proportional_fee: opening_fee_params.proportional, @@ -217,7 +98,7 @@ impl OutboundJITChannelState { } } - pub fn htlc_intercepted(&self, htlc: InterceptedHTLC) -> Result { + fn htlc_intercepted(&self, htlc: InterceptedHTLC) -> Result { match self { OutboundJITChannelState::AwaitingPayment { htlcs, @@ -278,7 +159,7 @@ impl OutboundJITChannelState { } } - pub fn channel_ready(&self) -> Result { + fn channel_ready(&self) -> Result { match self { OutboundJITChannelState::PendingChannelOpen { htlcs, amt_to_forward_msat, .. } => { Ok(OutboundJITChannelState::ChannelReady { @@ -302,7 +183,7 @@ struct OutboundJITChannel { } impl OutboundJITChannel { - pub fn new( + fn new( scid: u64, cltv_expiry_delta: u32, client_trusts_lsp: bool, payment_size_msat: Option, opening_fee_params: OpeningFeeParams, ) -> Self { @@ -314,7 +195,7 @@ impl OutboundJITChannel { } } - pub fn htlc_intercepted( + fn htlc_intercepted( &mut self, htlc: InterceptedHTLC, ) -> Result, LightningError> { self.state = self.state.htlc_intercepted(htlc)?; @@ -339,7 +220,7 @@ impl OutboundJITChannel { } } - pub fn channel_ready(&mut self) -> Result<(Vec, u64), LightningError> { + fn channel_ready(&mut self) -> Result<(Vec, u64), LightningError> { self.state = self.state.channel_ready()?; match &self.state { @@ -358,126 +239,71 @@ impl OutboundJITChannel { } struct PeerState { - inbound_channels_by_id: HashMap, outbound_channels_by_scid: HashMap, - request_to_cid: HashMap, pending_requests: HashMap, } impl PeerState { - pub fn new() -> Self { - let inbound_channels_by_id = HashMap::new(); + fn new() -> Self { let outbound_channels_by_scid = HashMap::new(); - let request_to_cid = HashMap::new(); let pending_requests = HashMap::new(); - Self { inbound_channels_by_id, outbound_channels_by_scid, request_to_cid, pending_requests } + Self { outbound_channels_by_scid, pending_requests } } - pub fn insert_inbound_channel(&mut self, jit_channel_id: u128, channel: InboundJITChannel) { - self.inbound_channels_by_id.insert(jit_channel_id, channel); - } - - pub fn insert_outbound_channel(&mut self, scid: u64, channel: OutboundJITChannel) { + fn insert_outbound_channel(&mut self, scid: u64, channel: OutboundJITChannel) { self.outbound_channels_by_scid.insert(scid, channel); } - pub fn insert_request(&mut self, request_id: RequestId, jit_channel_id: u128) { - self.request_to_cid.insert(request_id, jit_channel_id); - } - - pub fn remove_inbound_channel(&mut self, jit_channel_id: u128) { - self.inbound_channels_by_id.remove(&jit_channel_id); - } - - pub fn remove_outbound_channel(&mut self, scid: u64) { + fn remove_outbound_channel(&mut self, scid: u64) { self.outbound_channels_by_scid.remove(&scid); } } -pub struct JITChannelManager +/// The main object allowing to send and receive LSPS2 messages. +pub struct LSPS2ServiceHandler where - ES::Target: EntropySource, CM::Target: AChannelManager, - PM::Target: APeerManager, + MQ::Target: MessageQueue, { - entropy_source: ES, - peer_manager: Mutex>, channel_manager: CM, - pending_messages: Arc>>, + pending_messages: MQ, pending_events: Arc, per_peer_state: RwLock>>, peer_by_scid: RwLock>, - promise_secret: [u8; 32], - min_payment_size_msat: u64, - max_payment_size_msat: u64, + config: LSPS2ServiceConfig, } -impl JITChannelManager +impl LSPS2ServiceHandler where - ES::Target: EntropySource, CM::Target: AChannelManager, - PM::Target: APeerManager, + MQ::Target: MessageQueue, { + /// Constructs a `LSPS2ServiceHandler`. pub(crate) fn new( - entropy_source: ES, config: &JITChannelsConfig, - pending_messages: Arc>>, - pending_events: Arc, channel_manager: CM, + pending_messages: MQ, pending_events: Arc, channel_manager: CM, + config: LSPS2ServiceConfig, ) -> Self { Self { - entropy_source, - promise_secret: config.promise_secret, - min_payment_size_msat: config.min_payment_size_msat, - max_payment_size_msat: config.max_payment_size_msat, pending_messages, pending_events, per_peer_state: RwLock::new(HashMap::new()), peer_by_scid: RwLock::new(HashMap::new()), - peer_manager: Mutex::new(None), channel_manager, + config, } } - pub fn set_peer_manager(&self, peer_manager: PM) { - *self.peer_manager.lock().unwrap() = Some(peer_manager); - } - - pub fn create_invoice( - &self, counterparty_node_id: PublicKey, payment_size_msat: Option, - token: Option, user_channel_id: u128, - ) { - let jit_channel_id = self.generate_jit_channel_id(); - let channel = - InboundJITChannel::new(jit_channel_id, user_channel_id, payment_size_msat, token); - - 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); - - let request_id = self.generate_request_id(); - peer_state_lock.insert_request(request_id.clone(), jit_channel_id); - - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( - counterparty_node_id, - LSPS2Message::Request(request_id, LSPS2Request::GetVersions(GetVersionsRequest {})) - .into(), - )); - } - - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } - } - + /// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid. + /// + /// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event. + /// + /// [`LSPS2ServiceEvent::GetInfo`]: crate::lsps2::event::LSPS2ServiceEvent::GetInfo pub fn invalid_token_provided( - &self, counterparty_node_id: PublicKey, request_id: RequestId, + &self, counterparty_node_id: &PublicKey, request_id: RequestId, ) -> Result<(), APIError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); @@ -505,13 +331,18 @@ where } } + /// Used by LSP to provide fee parameters to a client requesting a JIT Channel. + /// + /// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event. + /// + /// [`LSPS2ServiceEvent::GetInfo`]: crate::lsps2::event::LSPS2ServiceEvent::GetInfo pub fn opening_fee_params_generated( - &self, counterparty_node_id: PublicKey, request_id: RequestId, + &self, counterparty_node_id: &PublicKey, request_id: RequestId, opening_fee_params_menu: Vec, ) -> Result<(), APIError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); @@ -520,10 +351,12 @@ where let response = LSPS2Response::GetInfo(GetInfoResponse { opening_fee_params_menu: opening_fee_params_menu .into_iter() - .map(|param| param.into_opening_fee_params(&self.promise_secret)) + .map(|param| { + param.into_opening_fee_params(&self.config.promise_secret) + }) .collect(), - min_payment_size_msat: self.min_payment_size_msat, - max_payment_size_msat: self.max_payment_size_msat, + min_payment_size_msat: self.config.min_payment_size_msat, + max_payment_size_msat: self.config.max_payment_size_msat, }); self.enqueue_response(counterparty_node_id, request_id, response); Ok(()) @@ -542,70 +375,18 @@ where } } - pub fn opening_fee_params_selected( - &self, counterparty_node_id: PublicKey, jit_channel_id: u128, - 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) - { - let version = match jit_channel.opening_fee_params_selected() { - Ok(version) => version, - Err(e) => { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(APIError::APIMisuseError { err: e.err }); - } - }; - - let request_id = self.generate_request_id(); - let payment_size_msat = jit_channel.config.payment_size_msat; - peer_state.insert_request(request_id.clone(), jit_channel_id); - - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( - counterparty_node_id, - LSPS2Message::Request( - request_id, - LSPS2Request::Buy(BuyRequest { - version, - opening_fee_params, - payment_size_msat, - }), - ) - .into(), - )); - } - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } - } 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), - }) - } - } - - Ok(()) - } - + /// Used by LSP to provide client with the scid and cltv_expiry_delta to use in their invoice. + /// + /// Should be called in response to receiving a [`LSPS2ServiceEvent::BuyRequest`] event. + /// + /// [`LSPS2ServiceEvent::BuyRequest`]: crate::lsps2::event::LSPS2ServiceEvent::BuyRequest pub fn invoice_parameters_generated( - &self, counterparty_node_id: PublicKey, request_id: RequestId, scid: u64, + &self, counterparty_node_id: &PublicKey, request_id: RequestId, scid: u64, cltv_expiry_delta: u32, client_trusts_lsp: bool, ) -> Result<(), APIError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); @@ -613,7 +394,7 @@ where Some(LSPS2Request::Buy(buy_request)) => { { let mut peer_by_scid = self.peer_by_scid.write().unwrap(); - peer_by_scid.insert(scid, counterparty_node_id); + peer_by_scid.insert(scid, *counterparty_node_id); } let outbound_jit_channel = OutboundJITChannel::new( @@ -649,7 +430,19 @@ where } } - pub(crate) fn htlc_intercepted( + /// Forward [`Event::HTLCIntercepted`] event parameters into this function. + /// + /// Will fail the intercepted HTLC if the scid matches a payment we are expecting + /// but the payment amount is incorrect or the expiry has passed. + /// + /// Will generate a [`LSPS2ServiceEvent::OpenChannel`] event if the scid matches a payment we are expected + /// and the payment amount is correct and the offer has not expired. + /// + /// Will do nothing if the scid does not match any of the ones we gave out. + /// + /// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted + /// [`LSPS2ServiceEvent::OpenChannel`]: crate::lsps2::event::LSPS2ServiceEvent::OpenChannel + pub fn htlc_intercepted( &self, scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, ) -> Result<(), APIError> { let peer_by_scid = self.peer_by_scid.read().unwrap(); @@ -662,12 +455,14 @@ where let htlc = InterceptedHTLC { intercept_id, expected_outbound_amount_msat }; match jit_channel.htlc_intercepted(htlc) { Ok(Some((opening_fee_msat, amt_to_forward_msat))) => { - self.enqueue_event(Event::LSPS2(LSPS2Event::OpenChannel { - their_network_key: counterparty_node_id.clone(), - amt_to_forward_msat, - opening_fee_msat, - user_channel_id: scid as u128, - })); + self.enqueue_event(Event::LSPS2Service( + LSPS2ServiceEvent::OpenChannel { + their_network_key: counterparty_node_id.clone(), + amt_to_forward_msat, + opening_fee_msat, + user_channel_id: scid as u128, + }, + )); } Ok(None) => {} Err(e) => { @@ -692,8 +487,13 @@ where Ok(()) } - // figure out which intercept id is waiting on this channel and enqueue ForwardInterceptedHTLC event - pub(crate) fn channel_ready( + /// Forward [`Event::ChannelReady`] event parameters into this function. + /// + /// Will forward the intercepted HTLC if it matches a channel + /// we need to forward a payment over otherwise it will be ignored. + /// + /// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady + pub fn channel_ready( &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, ) -> Result<(), APIError> { if let Ok(scid) = user_channel_id.try_into() { @@ -749,30 +549,11 @@ where Ok(()) } - 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) - } - - fn generate_request_id(&self) -> RequestId { - let bytes = self.entropy_source.get_secure_random_bytes(); - RequestId(utils::hex_str(&bytes[0..16])) - } - fn enqueue_response( - &self, counterparty_node_id: PublicKey, request_id: RequestId, response: LSPS2Response, + &self, counterparty_node_id: &PublicKey, request_id: RequestId, response: LSPS2Response, ) { - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages - .push((counterparty_node_id, LSPS2Message::Response(request_id, response).into())); - } - - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } + self.pending_messages + .enqueue(counterparty_node_id, LSPS2Message::Response(request_id, response).into()); } fn enqueue_event(&self, event: Event) { @@ -783,7 +564,7 @@ where &self, request_id: RequestId, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::GetVersions(GetVersionsResponse { versions: SUPPORTED_SPEC_VERSIONS.to_vec(), @@ -792,83 +573,12 @@ where Ok(()) } - fn handle_get_versions_response( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetVersionsResponse, - ) -> Result<(), LightningError> { - 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(); - - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { - err: format!( - "Received get_versions 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_versions response for an unknown channel: {:?}", - jit_channel_id, - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - let token = jit_channel.config.token.clone(); - - let version = match jit_channel.versions_received(result.versions) { - Ok(version) => version, - Err(e) => { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(e); - } - }; - - let request_id = self.generate_request_id(); - peer_state.insert_request(request_id.clone(), jit_channel_id); - - { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( - *counterparty_node_id, - LSPS2Message::Request( - request_id, - LSPS2Request::GetInfo(GetInfoRequest { version, token }), - ) - .into(), - )); - } - - if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { - peer_manager.as_ref().process_events(); - } - } - None => { - return Err(LightningError { - err: format!( - "Received get_versions response from unknown peer: {:?}", - counterparty_node_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }) - } - } - - Ok(()) - } - fn handle_get_info_request( &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetInfoRequest, ) -> Result<(), LightningError> { if !SUPPORTED_SPEC_VERSIONS.contains(¶ms.version) { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::GetInfoError(ResponseError { code: LSPS2_GET_INFO_REQUEST_INVALID_VERSION_ERROR_CODE, @@ -890,7 +600,7 @@ where .pending_requests .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); - self.enqueue_event(Event::LSPS2(LSPS2Event::GetInfo { + self.enqueue_event(Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { request_id, counterparty_node_id: *counterparty_node_id, version: params.version, @@ -899,102 +609,12 @@ where Ok(()) } - fn handle_get_info_response( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse, - ) -> Result<(), LightningError> { - 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(); - - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(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.enqueue_event(Event::LSPS2(LSPS2Event::GetInfoResponse { - 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, - })); - } - None => { - return Err(LightningError { - err: format!( - "Received get_info response from unknown peer: {:?}", - counterparty_node_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }) - } - } - - Ok(()) - } - - fn handle_get_info_error( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError, - ) -> Result<(), LightningError> { - 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(); - - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(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 => { - return Err(LightningError { err: format!("Received error response for a get_info request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); - } - } - } - fn handle_buy_request( &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: BuyRequest, ) -> Result<(), LightningError> { if !SUPPORTED_SPEC_VERSIONS.contains(¶ms.version) { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_INVALID_VERSION_ERROR_CODE, @@ -1009,9 +629,9 @@ where } if let Some(payment_size_msat) = params.payment_size_msat { - if payment_size_msat < self.min_payment_size_msat { + if payment_size_msat < self.config.min_payment_size_msat { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, @@ -1026,9 +646,9 @@ where }); } - if payment_size_msat > self.max_payment_size_msat { + if payment_size_msat > self.config.max_payment_size_msat { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, @@ -1051,7 +671,7 @@ where Some(opening_fee) => { if opening_fee >= payment_size_msat { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, @@ -1068,7 +688,7 @@ where } None => { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, @@ -1086,9 +706,9 @@ where // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. - if !is_valid_opening_fee_params(¶ms.opening_fee_params, &self.promise_secret) { + if !is_valid_opening_fee_params(¶ms.opening_fee_params, &self.config.promise_secret) { self.enqueue_response( - *counterparty_node_id, + counterparty_node_id, request_id, LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, @@ -1110,7 +730,7 @@ where .pending_requests .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); - self.enqueue_event(Event::LSPS2(LSPS2Event::BuyRequest { + self.enqueue_event(Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { request_id, version: params.version, counterparty_node_id: *counterparty_node_id, @@ -1120,114 +740,12 @@ where Ok(()) } - - fn handle_buy_response( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: BuyResponse, - ) -> Result<(), LightningError> { - 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(); - - let jit_channel_id = - peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { - err: format!( - "Received buy 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 buy response for an unknown channel: {:?}", - jit_channel_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - if let Err(e) = jit_channel.invoice_params_received( - result.client_trusts_lsp, - result.jit_channel_scid.clone(), - ) { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(e); - } - - if let Ok(scid) = result.jit_channel_scid.to_scid() { - self.enqueue_event(Event::LSPS2(LSPS2Event::InvoiceGenerationReady { - counterparty_node_id: *counterparty_node_id, - 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, - })); - } else { - return Err(LightningError { - err: format!( - "Received buy response with an invalid scid {:?}", - result.jit_channel_scid - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }); - } - } - None => { - return Err(LightningError { - err: format!( - "Received buy response from unknown peer: {:?}", - counterparty_node_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }); - } - } - Ok(()) - } - - fn handle_buy_error( - &self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError, - ) -> Result<(), LightningError> { - 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(); - - let jit_channel_id = - peer_state.request_to_cid.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 { - err: format!( - "Received buy error for an unknown channel: {:?}", - jit_channel_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - Ok(()) - } - None => { - return Err(LightningError { err: format!("Received error response for a buy request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); - } - } - } } -impl ProtocolMessageHandler - for JITChannelManager +impl ProtocolMessageHandler for LSPS2ServiceHandler where - ES::Target: EntropySource, CM::Target: AChannelManager, - PM::Target: APeerManager, + MQ::Target: MessageQueue, { type ProtocolMessage = LSPS2Message; const PROTOCOL_NUMBER: Option = Some(2); @@ -1247,23 +765,13 @@ where self.handle_buy_request(request_id, counterparty_node_id, params) } }, - LSPS2Message::Response(request_id, response) => match response { - LSPS2Response::GetVersions(result) => { - self.handle_get_versions_response(request_id, counterparty_node_id, result) - } - LSPS2Response::GetInfo(result) => { - self.handle_get_info_response(request_id, counterparty_node_id, result) - } - LSPS2Response::GetInfoError(error) => { - self.handle_get_info_error(request_id, counterparty_node_id, error) - } - LSPS2Response::Buy(result) => { - self.handle_buy_response(request_id, counterparty_node_id, result) - } - LSPS2Response::BuyError(error) => { - self.handle_buy_error(request_id, counterparty_node_id, error) - } - }, + _ => { + debug_assert!( + false, + "Service handler received LSPS2 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS2 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + } } } } diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..355e88f --- /dev/null +++ b/src/manager.rs @@ -0,0 +1,504 @@ +use crate::events::{Event, EventQueue}; +use crate::lsps0::client::LSPS0ClientHandler; +use crate::lsps0::msgs::{ + LSPS0Message, LSPSMessage, ProtocolMessageHandler, RawLSPSMessage, LSPS_MESSAGE_TYPE_ID, +}; +use crate::lsps0::service::LSPS0ServiceHandler; +use crate::message_queue::{DefaultMessageQueue, MessageQueue}; + +#[cfg(lsps1)] +use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler}; +#[cfg(lsps1)] +use crate::lsps1::msgs::LSPS1Message; +#[cfg(lsps1)] +use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler}; + +use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; +use crate::lsps2::msgs::LSPS2Message; +use crate::lsps2::service::{LSPS2ServiceConfig, LSPS2ServiceHandler}; +use crate::prelude::{HashMap, String, Vec}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; +use lightning::ln::channelmanager::{AChannelManager, ChainParameters}; +use lightning::ln::features::{InitFeatures, NodeFeatures}; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::ln::peer_handler::{APeerManager, CustomMessageHandler}; +use lightning::ln::wire::CustomMessageReader; +use lightning::sign::EntropySource; +use lightning::util::logger::Level; +use lightning::util::ser::Readable; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; +const LSPS_FEATURE_BIT: usize = 729; + +/// A server-side configuration for [`LiquidityManager`]. +/// +/// Allows end-users to configure options when using the [`LiquidityManager`] +/// to provide liquidity services to clients. +pub struct LiquidityServiceConfig { + /// Optional server-side configuration for LSPS1 channel requests. + #[cfg(lsps1)] + pub lsps1_service_config: Option, + /// Optional server-side configuration for JIT channels + /// should you want to support them. + pub lsps2_service_config: Option, +} + +/// A client-side configuration for [`LiquidityManager`]. +/// +/// Allows end-user to configure options when using the [`LiquidityManager`] +/// to access liquidity services from a provider. +pub struct LiquidityClientConfig { + /// Optional client-side configuration for LSPS1 channel requests. + #[cfg(lsps1)] + pub lsps1_client_config: Option, + /// Optional client-side configuration for JIT channels. + pub lsps2_client_config: Option, +} + +/// The main interface into LSP functionality. +/// +/// Should be used as a [`CustomMessageHandler`] for your +/// [`PeerManager`]'s [`MessageHandler`]. +/// +/// Should provide a reference to your [`PeerManager`] by calling +/// [`LiquidityManager::set_peer_manager`] post construction. This allows the [`LiquidityManager`] to +/// wake the [`PeerManager`] when there are pending messages to be sent. +/// +/// Users need to continually poll [`LiquidityManager::get_and_clear_pending_events`] in order to surface +/// [`Event`]'s that likely need to be handled. +/// +/// If configured, users must forward the [`Event::HTLCIntercepted`] event parameters to [`LSPS2ServiceHandler::htlc_intercepted`] +/// and the [`Event::ChannelReady`] event parameters to [`LSPS2ServiceHandler::channel_ready`]. +/// +/// [`PeerManager`]: lightning::ln::peer_handler::PeerManager +/// [`MessageHandler`]: lightning::ln::peer_handler::MessageHandler +/// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted +/// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady +pub struct LiquidityManager< + ES: Deref + Clone, + CM: Deref + Clone, + PM: Deref + Clone, + C: Deref + Clone, +> where + ES::Target: EntropySource, + CM::Target: AChannelManager, + PM::Target: APeerManager, + C::Target: Filter, +{ + pending_messages: Arc>, + pending_events: Arc, + request_id_to_method_map: Mutex>, + lsps0_client_handler: LSPS0ClientHandler>>, + lsps0_service_handler: Option>>>, + #[cfg(lsps1)] + lsps1_service_handler: Option>, C>>, + #[cfg(lsps1)] + lsps1_client_handler: Option>, C>>, + lsps2_service_handler: Option>>>, + lsps2_client_handler: Option>>>, + service_config: Option, + _client_config: Option, + best_block: Option>, + _chain_source: Option, +} + +impl + LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + PM::Target: APeerManager, + C::Target: Filter, +{ + /// Constructor for the [`LiquidityManager`]. + /// + /// Sets up the required protocol message handlers based on the given + /// [`LiquidityClientConfig`] and [`LiquidityServiceConfig`]. + pub fn new( + entropy_source: ES, channel_manager: CM, chain_source: Option, + chain_params: Option, service_config: Option, + client_config: Option, + ) -> Self +where { + let pending_messages = Arc::new(DefaultMessageQueue::new()); + let pending_events = Arc::new(EventQueue::new()); + + let lsps0_client_handler = LSPS0ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + ); + + let lsps0_service_handler = if service_config.is_some() { + Some(LSPS0ServiceHandler::new(vec![], Arc::clone(&pending_messages))) + } else { + None + }; + + let lsps2_client_handler = client_config.as_ref().and_then(|config| { + config.lsps2_client_config.map(|config| { + LSPS2ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + config.clone(), + ) + }) + }); + let lsps2_service_handler = service_config.as_ref().and_then(|config| { + config.lsps2_service_config.as_ref().map(|config| { + LSPS2ServiceHandler::new( + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + config.clone(), + ) + }) + }); + + #[cfg(lsps1)] + let lsps1_client_handler = client_config.as_ref().and_then(|config| { + config.lsps1_client_config.as_ref().map(|config| { + LSPS1ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + chain_source.clone(), + config.clone(), + ) + }) + }); + + #[cfg(lsps1)] + let lsps1_service_handler = service_config.as_ref().and_then(|config| { + config.lsps1_service_config.as_ref().map(|config| { + LSPS1ServiceHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + chain_source.clone(), + config.clone(), + ) + }) + }); + + Self { + pending_messages, + pending_events, + request_id_to_method_map: Mutex::new(HashMap::new()), + lsps0_client_handler, + lsps0_service_handler, + #[cfg(lsps1)] + lsps1_client_handler, + #[cfg(lsps1)] + lsps1_service_handler, + lsps2_client_handler, + lsps2_service_handler, + service_config, + _client_config: client_config, + best_block: chain_params.map(|chain_params| RwLock::new(chain_params.best_block)), + _chain_source: chain_source, + } + } + + /// Returns a reference to the LSPS0 client-side handler. + pub fn lsps0_client_handler(&self) -> &LSPS0ClientHandler>> { + &self.lsps0_client_handler + } + + /// Returns a reference to the LSPS0 server-side handler. + pub fn lsps0_service_handler( + &self, + ) -> Option<&LSPS0ServiceHandler>>> { + self.lsps0_service_handler.as_ref() + } + + /// Returns a reference to the LSPS1 client-side handler. + #[cfg(lsps1)] + pub fn lsps1_client_handler( + &self, + ) -> Option<&LSPS1ClientHandler>, C>> { + self.lsps1_client_handler.as_ref() + } + + /// Returns a reference to the LSPS1 server-side handler. + #[cfg(lsps1)] + pub fn lsps1_service_handler( + &self, + ) -> Option<&LSPS1ServiceHandler>, C>> { + self.lsps1_service_handler.as_ref() + } + + /// Returns a reference to the LSPS2 client-side handler. + pub fn lsps2_client_handler( + &self, + ) -> Option<&LSPS2ClientHandler>>> { + self.lsps2_client_handler.as_ref() + } + + /// Returns a reference to the LSPS2 server-side handler. + pub fn lsps2_service_handler( + &self, + ) -> Option<&LSPS2ServiceHandler>>> { + self.lsps2_service_handler.as_ref() + } + + /// Blocks the current thread until next event is ready and returns it. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + #[cfg(feature = "std")] + pub fn wait_next_event(&self) -> Event { + self.pending_events.wait_next_event() + } + + /// Returns `Some` if an event is ready. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + pub fn next_event(&self) -> Option { + self.pending_events.next_event() + } + + /// Returns and clears all events without blocking. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + pub fn get_and_clear_pending_events(&self) -> Vec { + self.pending_events.get_and_clear_pending_events() + } + + /// Set a [`PeerManager`] reference for all configured message handlers. + /// + /// This allows the message handlers to wake the [`PeerManager`] by calling + /// [`PeerManager::process_events`] after enqueing messages to be sent. + /// + /// Without this the messages will be sent based on whatever polling interval + /// your background processor uses. + /// + /// [`PeerManager`]: lightning::ln::peer_handler::PeerManager + /// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events + pub fn set_peer_manager(&self, peer_manager: PM) { + self.pending_messages.set_peer_manager(peer_manager); + } + + fn handle_lsps_message( + &self, msg: LSPSMessage, sender_node_id: &PublicKey, + ) -> Result<(), lightning::ln::msgs::LightningError> { + match msg { + LSPSMessage::Invalid => { + return Err(LightningError { err: format!("{} did not understand a message we previously sent, maybe they don't support a protocol we are trying to use?", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Error)}); + } + LSPSMessage::LSPS0(msg @ LSPS0Message::Response(..)) => { + self.lsps0_client_handler.handle_message(msg, sender_node_id)?; + } + LSPSMessage::LSPS0(msg @ LSPS0Message::Request(..)) => { + match &self.lsps0_service_handler { + Some(lsps0_service_handler) => { + lsps0_service_handler.handle_message(msg, sender_node_id)?; + } + None => { + return Err(LightningError { err: format!("Received LSPS0 request message without LSPS0 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + } + } + #[cfg(lsps1)] + LSPSMessage::LSPS1(msg @ LSPS1Message::Response(..)) => match &self.lsps1_client_handler { + Some(lsps1_client_handler) => { + lsps1_client_handler.handle_message(msg, sender_node_id)?; + } + None => { + return Err(LightningError { err: format!("Received LSPS1 response message without LSPS1 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + }, + #[cfg(lsps1)] + LSPSMessage::LSPS1(msg @ LSPS1Message::Request(..)) => match &self.lsps1_service_handler { + Some(lsps1_service_handler) => { + lsps1_service_handler.handle_message(msg, sender_node_id)?; + } + None => { + return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + }, + LSPSMessage::LSPS2(msg @ LSPS2Message::Response(..)) => { + match &self.lsps2_client_handler { + Some(lsps2_client_handler) => { + lsps2_client_handler.handle_message(msg, sender_node_id)?; + } + None => { + return Err(LightningError { err: format!("Received LSPS2 response message without LSPS2 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + } + } + LSPSMessage::LSPS2(msg @ LSPS2Message::Request(..)) => { + match &self.lsps2_service_handler { + Some(lsps2_service_handler) => { + lsps2_service_handler.handle_message(msg, sender_node_id)?; + } + None => { + return Err(LightningError { err: format!("Received LSPS2 request message without LSPS2 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + } + } + } + Ok(()) + } +} + +impl + CustomMessageReader for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + PM::Target: APeerManager, + C::Target: Filter, +{ + type CustomMessage = RawLSPSMessage; + + fn read( + &self, message_type: u16, buffer: &mut RD, + ) -> Result, lightning::ln::msgs::DecodeError> { + match message_type { + LSPS_MESSAGE_TYPE_ID => Ok(Some(RawLSPSMessage::read(buffer)?)), + _ => Ok(None), + } + } +} + +impl CustomMessageHandler + for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + PM::Target: APeerManager, + C::Target: Filter, +{ + fn handle_custom_message( + &self, msg: Self::CustomMessage, sender_node_id: &PublicKey, + ) -> Result<(), lightning::ln::msgs::LightningError> { + let message = { + let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); + LSPSMessage::from_str_with_id_map(&msg.payload, &mut request_id_to_method_map) + }; + + match message { + Ok(msg) => self.handle_lsps_message(msg, sender_node_id), + Err(_) => { + self.pending_messages.enqueue(sender_node_id, LSPSMessage::Invalid); + Ok(()) + } + } + } + + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Self::CustomMessage)> { + let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); + self.pending_messages + .get_and_clear_pending_msgs() + .iter() + .map(|(public_key, lsps_message)| { + if let Some((request_id, method_name)) = lsps_message.get_request_id_and_method() { + request_id_to_method_map.insert(request_id, method_name); + } + ( + *public_key, + RawLSPSMessage { payload: serde_json::to_string(&lsps_message).unwrap() }, + ) + }) + .collect() + } + + fn provided_node_features(&self) -> NodeFeatures { + let mut features = NodeFeatures::empty(); + + if self.service_config.is_some() { + features.set_optional_custom_bit(LSPS_FEATURE_BIT).unwrap(); + } + + features + } + + fn provided_init_features(&self, _their_node_id: &PublicKey) -> InitFeatures { + let mut features = InitFeatures::empty(); + + if self.service_config.is_some() { + features.set_optional_custom_bit(LSPS_FEATURE_BIT).unwrap(); + } + + features + } +} + +impl Listen + for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + PM::Target: APeerManager, + C::Target: Filter, +{ + fn filtered_block_connected( + &self, header: &bitcoin::BlockHeader, txdata: &chain::transaction::TransactionData, + height: u32, + ) { + if let Some(best_block) = &self.best_block { + let best_block = best_block.read().unwrap(); + assert_eq!(best_block.block_hash(), header.prev_blockhash, + "Blocks must be connected in chain-order - the connected header must build on the last connected header"); + assert_eq!(best_block.height(), height - 1, + "Blocks must be connected in chain-order - the connected block height must be one greater than the previous height"); + } + + self.transactions_confirmed(header, txdata, height); + self.best_block_updated(header, height); + } + + fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) { + let new_height = height - 1; + if let Some(best_block) = &self.best_block { + let mut best_block = best_block.write().unwrap(); + assert_eq!(best_block.block_hash(), header.block_hash(), + "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); + assert_eq!(best_block.height(), height, + "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); + *best_block = BestBlock::new(header.prev_blockhash, new_height) + } + + // TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler. + // Internally this should call transaction_unconfirmed for all transactions that were + // confirmed at a height <= the one we now disconnected. + } +} + +impl Confirm + for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + PM::Target: APeerManager, + C::Target: Filter, +{ + fn transactions_confirmed( + &self, header: &bitcoin::BlockHeader, txdata: &chain::transaction::TransactionData, + height: u32, + ) { + // TODO: Call transactions_confirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. + } + + fn transaction_unconfirmed(&self, txid: &bitcoin::Txid) { + // TODO: Call transaction_unconfirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. + // Internally this should call transaction_unconfirmed for all transactions that were + // confirmed at a height <= the one we now unconfirmed. + } + + fn best_block_updated(&self, header: &bitcoin::BlockHeader, height: u32) { + // TODO: Call best_block_updated on all sub-modules that require it, e.g., LSPS1MessageHandler. + } + + fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, Option)> { + // TODO: Collect relevant txids from all sub-modules that, e.g., LSPS1MessageHandler. + Vec::new() + } +} diff --git a/src/message_queue.rs b/src/message_queue.rs new file mode 100644 index 0000000..9cc420e --- /dev/null +++ b/src/message_queue.rs @@ -0,0 +1,66 @@ +//! Holds types and traits used to implement message queues for [`LSPSMessage`]s. + +use crate::lsps0::msgs::LSPSMessage; +use crate::prelude::{Vec, VecDeque}; +use crate::sync::Mutex; + +use lightning::ln::peer_handler::APeerManager; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +/// Represents a simple message queue that the LSPS message handlers use to send messages to a given counterparty. +pub trait MessageQueue { + /// Enqueues an LSPS message to be sent to the counterparty with the given node id. + /// + /// Implementations need to take care of message delivery to the counterparty via the + /// LSPS0/BOLT8 transport protocol. + fn enqueue(&self, counterparty_node_id: &PublicKey, msg: LSPSMessage); +} + +/// The default [`MessageQueue`] Implementation used by [`LiquidityManager`]. +/// +/// [`LiquidityManager`]: crate::LiquidityManager +pub struct DefaultMessageQueue +where + PM::Target: APeerManager, +{ + queue: Mutex>, + peer_manager: Mutex>, +} + +impl DefaultMessageQueue +where + PM::Target: APeerManager, +{ + pub(crate) fn new() -> Self { + let queue = Mutex::new(VecDeque::new()); + let peer_manager = Mutex::new(None); + Self { queue, peer_manager } + } + + pub(crate) fn get_and_clear_pending_msgs(&self) -> Vec<(PublicKey, LSPSMessage)> { + self.queue.lock().unwrap().drain(..).collect() + } + + pub(crate) fn set_peer_manager(&self, peer_manager: PM) { + *self.peer_manager.lock().unwrap() = Some(peer_manager); + } +} + +impl MessageQueue for DefaultMessageQueue +where + PM::Target: APeerManager, +{ + fn enqueue(&self, counterparty_node_id: &PublicKey, msg: LSPSMessage) { + { + let mut queue = self.queue.lock().unwrap(); + queue.push_back((*counterparty_node_id, msg)); + } + + if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { + peer_manager.as_ref().process_events(); + } + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..b5614dd --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1 @@ +pub mod utils; diff --git a/src/tests/utils.rs b/src/tests/utils.rs new file mode 100644 index 0000000..4110e74 --- /dev/null +++ b/src/tests/utils.rs @@ -0,0 +1,39 @@ +use crate::lsps0::msgs::LSPSMessage; +use crate::message_queue::MessageQueue; +use crate::prelude::{Vec, VecDeque}; +use crate::sync::Mutex; + +use lightning::sign::EntropySource; + +use bitcoin::secp256k1::PublicKey; + +pub(crate) struct TestMessageQueue { + queue: Mutex>, +} + +impl TestMessageQueue { + pub(crate) fn new() -> Self { + let queue = Mutex::new(VecDeque::new()); + Self { queue } + } + + pub(crate) fn get_and_clear_pending_msgs(&self) -> Vec<(PublicKey, LSPSMessage)> { + self.queue.lock().unwrap().drain(..).collect() + } +} + +impl MessageQueue for TestMessageQueue { + fn enqueue(&self, counterparty_node_id: &PublicKey, msg: LSPSMessage) { + { + let mut queue = self.queue.lock().unwrap(); + queue.push_back((*counterparty_node_id, msg)); + } + } +} + +pub struct TestEntropy {} +impl EntropySource for TestEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [0; 32] + } +}