diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20b78cd..9e66a72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,8 +28,6 @@ jobs: - name: Pin crates for MSRV if: matrix.msrv run: | - # Tokio MSRV on versions 1.17 through 1.26 is rustc 1.49. Above 1.26 MSRV is 1.56. - cargo update -p tokio --precise "1.14.1" --verbose # The serde_json crate switched to Rust edition 2021 starting with v1.0.101, i.e., has MSRV of 1.56 cargo update -p serde_json --precise "1.0.99" --verbose cargo update -p serde --precise "1.0.156" --verbose @@ -37,10 +35,7 @@ jobs: cargo update -p quote --precise "1.0.30" --verbose # The proc-macro2 crate switched to Rust edition 2021 starting with v1.0.66, i.e., has MSRV of 1.56 cargo update -p proc-macro2 --precise "1.0.65" --verbose - # Sadly the log crate is always a dependency of tokio until 1.20, and has no reasonable MSRV guarantees - cargo update -p log --precise "0.4.18" --verbose - # The memchr crate switched to Rust edition 2021 starting with v2.6.0 - cargo update -p memchr --precise "2.5.0" --verbose + cargo update -p chrono --precise "0.4.24" --verbose - name: Cargo check run: cargo check --release - name: Check documentation diff --git a/Cargo.toml b/Cargo.toml index eee987c..18fcb08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "ldk-lsp-client" version = "0.1.0" -authors = ["Elias Rohrer "] +authors = ["John Cantrell ", "Elias Rohrer "] edition = "2018" description = "Types and primitives to integrate a spec-compliant LSP with an LDK-based node." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lightning = { version = "0.0.116", features = ["max_level_trace", "std"] } -lightning-invoice = "0.24.0" -lightning-net-tokio = "0.0.116" +lightning = { version = "0.0.117", default-features = false, features = ["max_level_trace", "std"] } +lightning-invoice = "0.25.0" bitcoin = "0.29.0" +chrono = { version = "0.4", default-features = false, features = ["std", "serde"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" diff --git a/src/channel_request/mod.rs b/src/channel_request/mod.rs index 673242d..b3b102c 100644 --- a/src/channel_request/mod.rs +++ b/src/channel_request/mod.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// 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. + +use std::collections::HashMap; +use std::convert::TryInto; +use std::ops::Deref; +use std::sync::{Arc, Mutex, RwLock}; + +use bitcoin::secp256k1::PublicKey; +use lightning::chain; +use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; +use lightning::ln::channelmanager::{ChannelManager, InterceptId}; +use lightning::ln::msgs::{ + ChannelMessageHandler, ErrorAction, LightningError, OnionMessageHandler, RoutingMessageHandler, +}; +use lightning::ln::peer_handler::{CustomMessageHandler, PeerManager, SocketDescriptor}; +use lightning::ln::ChannelId; +use lightning::routing::router::Router; +use lightning::sign::{EntropySource, NodeSigner, SignerProvider}; +use lightning::util::errors::APIError; +use lightning::util::logger::{Level, Logger}; + +use crate::events::EventQueue; +use crate::jit_channel::utils::{compute_opening_fee, is_valid_opening_fee_params}; +use crate::jit_channel::LSPS2Event; +use crate::transport::message_handler::ProtocolMessageHandler; +use crate::transport::msgs::{LSPSMessage, RequestId}; +use crate::{events::Event, transport::msgs::ResponseError}; +use crate::{utils, JITChannelsConfig}; + +use crate::jit_channel::msgs::{ + BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, GetVersionsRequest, + GetVersionsResponse, JitChannelScid, 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, +}; + +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 { + 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 { + InvoiceParametersGenerated { + short_channel_id: u64, + cltv_expiry_delta: u32, + payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + }, + PendingChannelOpen { + intercept_id: InterceptId, + opening_fee_msat: u64, + amt_to_forward_msat: u64, + }, + ChannelReady { + intercept_id: InterceptId, + amt_to_forward_msat: u64, + }, +} + +impl OutboundJITChannelState { + pub fn new( + short_channel_id: u64, cltv_expiry_delta: u32, payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + ) -> Self { + OutboundJITChannelState::InvoiceParametersGenerated { + short_channel_id, + cltv_expiry_delta, + payment_size_msat, + opening_fee_params, + } + } + + pub fn htlc_intercepted( + &self, expected_outbound_amount_msat: u64, intercept_id: InterceptId, + ) -> Result { + match self { + OutboundJITChannelState::InvoiceParametersGenerated { opening_fee_params, .. } => { + compute_opening_fee( + expected_outbound_amount_msat, + opening_fee_params.min_fee_msat, + opening_fee_params.proportional, + ).map(|opening_fee_msat| OutboundJITChannelState::PendingChannelOpen { + intercept_id, + opening_fee_msat, + amt_to_forward_msat: expected_outbound_amount_msat - opening_fee_msat, + }).ok_or(ChannelStateError(format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and expected_outbound_amount_msat = {}", opening_fee_params.min_fee_msat, opening_fee_params.proportional, expected_outbound_amount_msat))) + } + state => Err(ChannelStateError(format!( + "Invoice params received when JIT Channel was in state: {:?}", + state + ))), + } + } + + pub fn channel_ready(&self) -> Result { + match self { + OutboundJITChannelState::PendingChannelOpen { + intercept_id, + amt_to_forward_msat, + .. + } => Ok(OutboundJITChannelState::ChannelReady { + intercept_id: *intercept_id, + amt_to_forward_msat: *amt_to_forward_msat, + }), + state => Err(ChannelStateError(format!( + "Channel ready received when JIT Channel was in state: {:?}", + state + ))), + } + } +} + +struct OutboundJITChannel { + state: OutboundJITChannelState, +} + +impl OutboundJITChannel { + pub fn new( + scid: u64, cltv_expiry_delta: u32, payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + ) -> Self { + Self { + state: OutboundJITChannelState::new( + scid, + cltv_expiry_delta, + payment_size_msat, + opening_fee_params, + ), + } + } + + pub fn htlc_intercepted( + &mut self, expected_outbound_amount_msat: u64, intercept_id: InterceptId, + ) -> Result<(u64, u64), LightningError> { + self.state = self.state.htlc_intercepted(expected_outbound_amount_msat, intercept_id)?; + + match &self.state { + OutboundJITChannelState::PendingChannelOpen { + opening_fee_msat, + amt_to_forward_msat, + .. + } => Ok((*opening_fee_msat, *amt_to_forward_msat)), + impossible_state => Err(LightningError { + err: format!( + "Impossible state transition during htlc_intercepted to {:?}", + impossible_state + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }), + } + } + + pub fn channel_ready(&mut self) -> Result<(InterceptId, u64), LightningError> { + self.state = self.state.channel_ready()?; + + match &self.state { + OutboundJITChannelState::ChannelReady { intercept_id, amt_to_forward_msat } => { + Ok((*intercept_id, *amt_to_forward_msat)) + } + impossible_state => Err(LightningError { + err: format!( + "Impossible state transition during channel_ready to {:?}", + impossible_state + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }), + } + } +} + +#[derive(Default)] +struct PeerState { + inbound_channels_by_id: HashMap, + outbound_channels_by_scid: HashMap, + request_to_cid: HashMap, + pending_requests: HashMap, +} + +impl PeerState { + 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) { + 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) { + self.outbound_channels_by_scid.remove(&scid); + } +} + +pub struct JITChannelManager< + ES: Deref, + M: Deref, + T: Deref, + F: Deref, + R: Deref, + SP: Deref, + Descriptor: SocketDescriptor, + L: Deref, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, + NS: Deref, +> where + ES::Target: EntropySource, + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + F::Target: FeeEstimator, + R::Target: Router, + SP::Target: SignerProvider, + L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, + NS::Target: NodeSigner, +{ + entropy_source: ES, + peer_manager: Mutex>>>, + channel_manager: Arc>, + pending_messages: Arc>>, + 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, +} + +impl< + ES: Deref, + M: Deref, + T: Deref, + F: Deref, + R: Deref, + SP: Deref, + Descriptor: SocketDescriptor, + L: Deref, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, + NS: Deref, + > JITChannelManager +where + ES::Target: EntropySource, + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + F::Target: FeeEstimator, + R::Target: Router, + SP::Target: SignerProvider, + L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, + NS::Target: NodeSigner, +{ + pub(crate) fn new( + entropy_source: ES, config: &JITChannelsConfig, + pending_messages: Arc>>, + pending_events: Arc, + channel_manager: Arc>, + ) -> 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, + } + } + + pub fn set_peer_manager( + &self, peer_manager: Arc>, + ) { + *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::default())); + let peer_state = inner_state_lock.get_mut().unwrap(); + peer_state.insert_inbound_channel(jit_channel_id, channel); + + 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::GetVersions(GetVersionsRequest {})) + .into(), + )); + } + + if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { + peer_manager.process_events(); + } + } + + pub fn opening_fee_params_generated( + &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) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + match peer_state.pending_requests.remove(&request_id) { + Some(LSPS2Request::GetInfo(_)) => { + 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)) + .collect(), + min_payment_size_msat: self.min_payment_size_msat, + max_payment_size_msat: self.max_payment_size_msat, + }); + self.enqueue_response(counterparty_node_id, request_id, response); + Ok(()) + } + _ => Err(APIError::APIMisuseError { + err: format!( + "No pending get_info request for request_id: {:?}", + request_id + ), + }), + } + } + None => Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {:?}", counterparty_node_id), + }), + } + } + + 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.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(()) + } + + 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> { + 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(); + + match peer_state.pending_requests.remove(&request_id) { + Some(LSPS2Request::Buy(buy_request)) => { + { + let mut peer_by_scid = self.peer_by_scid.write().unwrap(); + peer_by_scid.insert(scid, counterparty_node_id); + } + + let outbound_jit_channel = OutboundJITChannel::new( + scid, + cltv_expiry_delta, + buy_request.payment_size_msat, + buy_request.opening_fee_params, + ); + + peer_state.insert_outbound_channel(scid, outbound_jit_channel); + + self.enqueue_response( + counterparty_node_id, + request_id, + LSPS2Response::Buy(BuyResponse { + jit_channel_scid: scid.into(), + lsp_cltv_expiry_delta: cltv_expiry_delta, + client_trusts_lsp, + }), + ); + + Ok(()) + } + _ => Err(APIError::APIMisuseError { + err: format!("No pending buy request for request_id: {:?}", request_id), + }), + } + } + None => Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {:?}", counterparty_node_id), + }), + } + } + + pub(crate) fn htlc_intercepted( + &self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64, + expected_outbound_amount_msat: u64, + ) -> Result<(), APIError> { + let peer_by_scid = self.peer_by_scid.read().unwrap(); + if let Some(counterparty_node_id) = peer_by_scid.get(&scid) { + 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.outbound_channels_by_scid.get_mut(&scid) { + // TODO: Need to support MPP payments. If payment_amount_msat is known, needs to queue intercepted HTLCs in a map by payment_hash + // LiquidityManager will need to be regularly polled so it can continually check if the payment amount has been received + // and can release the payment or if the channel valid_until has expired and should be failed. + // Can perform check each time HTLC is received and on interval? I guess interval only needs to check expiration as + // we can only reach threshold when htlc is intercepted. + + match jit_channel + .htlc_intercepted(expected_outbound_amount_msat, intercept_id) + { + Ok((opening_fee_msat, amt_to_forward_msat)) => { + self.enqueue_event(Event::LSPS2(LSPS2Event::OpenChannel { + their_network_key: counterparty_node_id.clone(), + inbound_amount_msat, + expected_outbound_amount_msat, + amt_to_forward_msat, + opening_fee_msat, + user_channel_id: scid as u128, + })); + } + Err(e) => { + self.channel_manager.fail_intercepted_htlc(intercept_id)?; + peer_state.outbound_channels_by_scid.remove(&scid); + // TODO: cleanup peer_by_scid + return Err(APIError::APIMisuseError { err: e.err }); + } + } + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty found for scid: {}", scid), + }); + } + } + } + + Ok(()) + } + + // figure out which intercept id is waiting on this channel and enqueue ForwardInterceptedHTLC event + pub(crate) 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() { + 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.outbound_channels_by_scid.get_mut(&scid) { + match jit_channel.channel_ready() { + Ok((intercept_id, amt_to_forward_msat)) => { + self.channel_manager.forward_intercepted_htlc( + intercept_id, + channel_id, + *counterparty_node_id, + amt_to_forward_msat, + )?; + } + Err(e) => { + return Err(APIError::APIMisuseError { + err: format!( + "Failed to transition to channel ready: {}", + e.err + ), + }) + } + } + } else { + return Err(APIError::APIMisuseError { + err: format!( + "Could not find a channel with user_channel_id {}", + user_channel_id + ), + }); + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty state for: {}", 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 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, + ) { + { + 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.process_events(); + } + } + + fn enqueue_event(&self, event: Event) { + self.pending_events.enqueue(event); + } + + fn handle_get_versions_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::GetVersions(GetVersionsResponse { + versions: SUPPORTED_SPEC_VERSIONS.to_vec(), + }), + ); + 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.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> { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock: &mut Mutex = outer_state_lock + .entry(*counterparty_node_id) + .or_insert(Mutex::new(PeerState::default())); + let peer_state = inner_state_lock.get_mut().unwrap(); + peer_state + .pending_requests + .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); + + self.enqueue_event(Event::LSPS2(LSPS2Event::GetInfo { + request_id, + counterparty_node_id: *counterparty_node_id, + version: params.version, + token: params.token, + })); + 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: 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, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_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 let Some(payment_size_msat) = params.payment_size_msat { + if payment_size_msat < self.min_payment_size_msat { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, + message: "payment size is below our minimum supported payment size" + .to_string(), + data: None, + }), + ); + return Err(LightningError { + err: "payment size is below our minimum supported payment size".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + if payment_size_msat > self.max_payment_size_msat { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + message: "payment size is above our maximum supported payment size" + .to_string(), + data: None, + }), + ); + return Err(LightningError { + err: "payment size is above our maximum supported payment size".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + match compute_opening_fee( + payment_size_msat, + params.opening_fee_params.min_fee_msat, + params.opening_fee_params.proportional, + ) { + Some(opening_fee) => { + if opening_fee >= payment_size_msat { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, + message: "payment size is too small to cover the opening fee" + .to_string(), + data: None, + }), + ); + return Err(LightningError { + err: "payment size is too small to cover the opening fee".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + } + None => { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + message: "overflow error when calculating opening_fee".to_string(), + data: None, + }), + ); + return Err(LightningError { + err: "overflow error when calculating opening_fee".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + } + } + + // 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) { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, + message: "valid_until is already past OR the promise did not match the provided parameters".to_string(), + data: None, + }), + ); + return Err(LightningError { + err: "invalid opening fee parameters were supplied by client".to_string(), + 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 peer_state = inner_state_lock.get_mut().unwrap(); + peer_state.pending_requests.insert(request_id.clone(), LSPS2Request::Buy(params.clone())); + + self.enqueue_event(Event::LSPS2(LSPS2Event::BuyRequest { + request_id, + version: params.version, + counterparty_node_id: *counterparty_node_id, + opening_fee_params: params.opening_fee_params, + payment_size_msat: params.payment_size_msat, + })); + + 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< + ES: Deref, + M: Deref, + T: Deref, + F: Deref, + R: Deref, + SP: Deref, + Descriptor: SocketDescriptor, + L: Deref, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, + NS: Deref, + > ProtocolMessageHandler + for JITChannelManager +where + ES::Target: EntropySource, + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + F::Target: FeeEstimator, + R::Target: Router, + SP::Target: SignerProvider, + L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, + NS::Target: NodeSigner, +{ + 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::Request(request_id, request) => match request { + LSPS2Request::GetVersions(_) => { + self.handle_get_versions_request(request_id, counterparty_node_id) + } + LSPS2Request::GetInfo(params) => { + self.handle_get_info_request(request_id, counterparty_node_id, params) + } + LSPS2Request::Buy(params) => { + 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) + } + }, + } + } +} diff --git a/src/jit_channel/event.rs b/src/jit_channel/event.rs new file mode 100644 index 0000000..116e22a --- /dev/null +++ b/src/jit_channel/event.rs @@ -0,0 +1,124 @@ +// 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. + +use bitcoin::secp256k1::PublicKey; + +use super::msgs::OpeningFeeParams; +use crate::transport::msgs::RequestId; + +/// An event which you should probably 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`]. + /// + /// [`LiquidityManager::opening_fee_params_generated`]: crate::LiquidityManager::opening_fee_params_generated + 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, + }, + /// 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 want to use if you wish to proceed opening a channel. + /// + /// [`LiquidityManager::opening_fee_params_selected`]: crate::LiquidityManager::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`]. + /// + /// [`LiquidityManager::opening_fee_params_selected`]: crate::LiquidityManager::opening_fee_params_selected + jit_channel_id: u128, + /// The node id of the LSP that provided this response. + counterparty_node_id: PublicKey, + /// The menu of fee parameters the LSP is offering at this time. + /// You must select one of these if you wish to proceed. + opening_fee_params_menu: Vec, + /// The min payment size allowed when opening the channel. + 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::jit_channel_create_invoice`]. + /// + /// [`LiquidityManager::jit_channel_create_invoice`]: crate::LiquidityManager::jit_channel_create_invoice + user_channel_id: u128, + }, + /// A client has selected a opening fee parameter to use and would like to + /// purchase a channel with an optional initial payment size. + /// + /// If `payment_size_msat` is [`Option::Some`] then the payer is allowed to use MPP. + /// 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`]. + /// + /// [`LiquidityManager::invoice_parameters_generated`]: crate::LiquidityManager::invoice_parameters_generated + BuyRequest { + /// An identifier that must be passed into [`LiquidityManager::invoice_parameters_generated`]. + /// + /// [`LiquidityManager::invoice_parameters_generated`]: crate::LiquidityManager::invoice_parameters_generated + request_id: RequestId, + /// The client node id that is making this request. + counterparty_node_id: PublicKey, + /// The version of the protocol they would like to use. + version: u16, + /// The channel parameters they have selected. + opening_fee_params: OpeningFeeParams, + /// 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::jit_channel_create_invoice`]. + /// + /// [`LiquidityManager::jit_channel_create_invoice`]: crate::LiquidityManager::jit_channel_create_invoice + user_channel_id: u128, + }, + /// You should open a channel using [`ChannelManager::create_channel`]. + /// + /// [`ChannelManager::create_channel`]: lightning::ln::channelmanager::ChannelManager::create_channel + OpenChannel { + /// The node to open channel with. + their_network_key: PublicKey, + /// The intercepted HTLC amount in msats. + inbound_amount_msat: u64, + /// The amount the client expects to receive before fees are taken out. + expected_outbound_amount_msat: u64, + /// The amount to forward after fees. + amt_to_forward_msat: u64, + /// The fee earned for opening the channel. + opening_fee_msat: u64, + /// An internal id used to track channel open. + user_channel_id: u128, + }, +} diff --git a/src/jit_channel/mod.rs b/src/jit_channel/mod.rs index 0d948d9..2cee68b 100644 --- a/src/jit_channel/mod.rs +++ b/src/jit_channel/mod.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 , +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// A request made to an LSP to learn their current channel fees and parameters. +pub struct GetInfoRequest { + /// What version of the protocol we want to use. + pub version: u16, + /// An optional token to provide to the LSP. + pub token: Option, +} + +/// Fees and parameters for a JIT Channel without the promise. +/// +/// The promise will be calculated automatically for the LSP and this type converted +/// into an [`OpeningFeeParams`] for transit over the wire. +pub struct RawOpeningFeeParams { + /// The minimum fee required for the channel open. + pub min_fee_msat: u64, + /// A fee proportional to the size of the initial payment. + pub proportional: u32, + /// An [`ISO8601`](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date for which these params are valid. + pub valid_until: chrono::DateTime, + /// The number of blocks after confirmation that the LSP promises it will keep the channel alive without closing. + pub min_lifetime: u32, + /// T maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, +} + +impl RawOpeningFeeParams { + pub(crate) fn into_opening_fee_params(self, promise_secret: &[u8; 32]) -> OpeningFeeParams { + let mut hmac = HmacEngine::::new(promise_secret); + hmac.input(&self.min_fee_msat.to_be_bytes()); + hmac.input(&self.proportional.to_be_bytes()); + hmac.input(self.valid_until.to_rfc3339().as_bytes()); + hmac.input(&self.min_lifetime.to_be_bytes()); + hmac.input(&self.max_client_to_self_delay.to_be_bytes()); + let promise_bytes = Hmac::from_engine(hmac).into_inner(); + let promise = utils::hex_str(&promise_bytes[..]); + OpeningFeeParams { + min_fee_msat: self.min_fee_msat, + proportional: self.proportional, + valid_until: self.valid_until.clone(), + min_lifetime: self.min_lifetime, + max_client_to_self_delay: self.max_client_to_self_delay, + promise, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// Fees and parameters for a JIT Channel including the promise. +/// +/// The promise is an HMAC calculated using a secret known to the LSP and the rest of the fields as input. +/// It exists so the LSP can verify the authenticity of a client provided OpeningFeeParams by recalculating +/// the promise using the secret. Once verified they can be confident it was not modified by the client. +pub struct OpeningFeeParams { + /// The minimum fee required for the channel open. + pub min_fee_msat: u64, + /// A fee proportional to the size of the initial payment. + pub proportional: u32, + /// An [`ISO8601`](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date for which these params are valid. + pub valid_until: chrono::DateTime, + /// The number of blocks after confirmation that the LSP promises it will keep the channel alive without closing. + pub min_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The HMAC used to verify the authenticity of these parameters. + pub promise: String, +} + +/// A response to a [`GetInfoRequest`] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetInfoResponse { + /// A set of opening fee parameters. + pub opening_fee_params_menu: Vec, + /// The minimum payment size required to open a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size the lsp will tolerate. + pub max_payment_size_msat: u64, +} + +/// A request to buy a JIT channel. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct BuyRequest { + /// The version of the protocol to use. + pub version: u16, + /// The fee parameters you would like to use. + pub opening_fee_params: OpeningFeeParams, + /// The size of the initial payment you expect to receive. + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_size_msat: Option, +} + +/// 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); + +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); + let vout = utils::vout_from_scid(&scid); + + Self(format!("{}x{}x{}", block, tx_index, vout)) + } +} + +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) + } +} + +/// A response to a [`BuyRequest`]. +/// +/// Includes information needed to construct an invoice. +#[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, + /// The locktime expiry delta the lsp requires. + pub lsp_cltv_expiry_delta: u32, + /// A flag that indicates who is trusting who. + #[serde(default)] + pub client_trusts_lsp: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all the valid JSON-RPC requests in the LSPS2 protocol. +pub enum LSPS2Request { + /// A request to learn what versions an LSP supports. + GetVersions(GetVersionsRequest), + /// A request to learn an LSP's channel fees and parameters. + GetInfo(GetInfoRequest), + /// A request to buy a JIT channel from an LSP. + Buy(BuyRequest), +} + +impl LSPS2Request { + /// Get the JSON-RPC method name for the underlying request. + pub fn method(&self) -> &str { + match self { + LSPS2Request::GetVersions(_) => LSPS2_GET_VERSIONS_METHOD_NAME, + LSPS2Request::GetInfo(_) => LSPS2_GET_INFO_METHOD_NAME, + LSPS2Request::Buy(_) => LSPS2_BUY_METHOD_NAME, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all the valid JSON-RPC responses in the LSPS2 protocol. +pub enum LSPS2Response { + /// A successful response to a [`LSPS2Request::GetVersions`] request. + GetVersions(GetVersionsResponse), + /// A successful response to a [`LSPS2Request::GetInfo`] request. + GetInfo(GetInfoResponse), + /// An error response to a [`LSPS2Request::GetInfo`] request. + GetInfoError(ResponseError), + /// A successful response to a [`LSPS2Request::Buy`] request. + Buy(BuyResponse), + /// An error response to a [`LSPS2Request::Buy`] request. + BuyError(ResponseError), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all valid JSON-RPC messages in the LSPS2 protocol. +pub enum LSPS2Message { + /// An LSPS2 JSON-RPC request. + Request(RequestId, LSPS2Request), + /// An LSPS2 JSON-RPC response. + Response(RequestId, LSPS2Response), +} + +impl TryFrom for LSPS2Message { + type Error = (); + + fn try_from(message: LSPSMessage) -> Result { + if let LSPSMessage::LSPS2(message) = message { + return Ok(message); + } + + Err(()) + } +} + +impl From for LSPSMessage { + fn from(message: LSPS2Message) -> Self { + LSPSMessage::LSPS2(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::jit_channel::utils::is_valid_opening_fee_params; + + #[test] + fn into_opening_fee_params_produces_valid_promise() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until: chrono::DateTime = + chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap().into(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.clone().into(), + min_lifetime, + max_client_to_self_delay, + }; + + let promise_secret = [1u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + + assert_eq!(opening_fee_params.min_fee_msat, min_fee_msat); + assert_eq!(opening_fee_params.proportional, proportional); + assert_eq!(opening_fee_params.valid_until, valid_until); + assert_eq!(opening_fee_params.min_lifetime, min_lifetime); + assert_eq!(opening_fee_params.max_client_to_self_delay, max_client_to_self_delay); + + assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + } + + #[test] + fn changing_single_field_produced_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + }; + + let promise_secret = [1u8; 32]; + + let mut opening_fee_params = raw.into_opening_fee_params(&promise_secret); + opening_fee_params.min_fee_msat = min_fee_msat + 1; + assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + } + + #[test] + fn wrong_secret_produced_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + }; + + let promise_secret = [1u8; 32]; + let other_secret = [2u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &other_secret)); + } + + #[test] + fn expired_params_produces_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + }; + + let promise_secret = [1u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + } +} diff --git a/src/jit_channel/utils.rs b/src/jit_channel/utils.rs new file mode 100644 index 0000000..22354de --- /dev/null +++ b/src/jit_channel/utils.rs @@ -0,0 +1,52 @@ +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::{Hash, HashEngine}; + +use std::convert::TryInto; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::jit_channel::msgs::OpeningFeeParams; +use crate::utils; + +/// Determines if the given parameters are valid given the secret used to generate the promise. +pub fn is_valid_opening_fee_params( + fee_params: &OpeningFeeParams, promise_secret: &[u8; 32], +) -> bool { + let seconds_since_epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock to be ahead of the unix epoch") + .as_secs(); + let valid_until_seconds_since_epoch = fee_params + .valid_until + .timestamp() + .try_into() + .expect("expiration to be ahead of unix epoch"); + if seconds_since_epoch > valid_until_seconds_since_epoch { + return false; + } + + let mut hmac = HmacEngine::::new(promise_secret); + hmac.input(&fee_params.min_fee_msat.to_be_bytes()); + hmac.input(&fee_params.proportional.to_be_bytes()); + hmac.input(fee_params.valid_until.to_rfc3339().as_bytes()); + hmac.input(&fee_params.min_lifetime.to_be_bytes()); + hmac.input(&fee_params.max_client_to_self_delay.to_be_bytes()); + let promise_bytes = Hmac::from_engine(hmac).into_inner(); + let promise = utils::hex_str(&promise_bytes[..]); + promise == fee_params.promise +} + +/// Computes the opening fee given a payment size and the fee parameters. +/// +/// Returns [`Option::None`] when the computation overflows. +/// +/// See the [`specification`](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#computing-the-opening_fee) for more details. +pub fn compute_opening_fee( + payment_size_msat: u64, opening_fee_min_fee_msat: u64, opening_fee_proportional: u32, +) -> Option { + let t1 = payment_size_msat.checked_mul(opening_fee_proportional.into())?; + let t2 = t1.checked_add(999999)?; + let t3 = t2.checked_div(1000000)?; + let t4 = std::cmp::max(t3, opening_fee_min_fee_msat); + Some(t4) +} diff --git a/src/lib.rs b/src/lib.rs index 4919017..6c928fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 , +} + +/// 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, +} /// The main interface into LSP functionality. /// /// Should be used as a [`CustomMessageHandler`] for your -/// [`lightning::ln::peer_handler::PeerManager`]'s [`lightning::ln::peer_handler::MessageHandler`]. +/// [`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, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, > where @@ -67,6 +109,10 @@ pub struct LiquidityManager< R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { @@ -74,6 +120,8 @@ pub struct LiquidityManager< pending_events: Arc, request_id_to_method_map: Mutex>, lsps0_message_handler: LSPS0MessageHandler, + lsps2_message_handler: + Option>, provider_config: Option, channel_manager: Arc>, chain_source: Option, @@ -82,16 +130,21 @@ pub struct LiquidityManager< } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > LiquidityManager + > LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -100,10 +153,14 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { - /// Constructor for the LiquidityManager + /// Constructor for the [`LiquidityManager`]. /// /// Sets up the required protocol message handlers based on the given [`LiquidityProviderConfig`]. pub fn new( @@ -113,15 +170,29 @@ where ) -> Self where { let pending_messages = Arc::new(Mutex::new(vec![])); + let pending_events = Arc::new(EventQueue::default()); let lsps0_message_handler = - LSPS0MessageHandler::new(entropy_source, vec![], Arc::clone(&pending_messages)); + LSPS0MessageHandler::new(entropy_source.clone(), vec![], Arc::clone(&pending_messages)); + + let lsps2_message_handler = provider_config.as_ref().and_then(|config| { + config.jit_channels.as_ref().map(|jit_channels_config| { + JITChannelManager::new( + entropy_source.clone(), + jit_channels_config, + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + Arc::clone(&channel_manager), + ) + }) + }); Self { pending_messages, - pending_events: Arc::new(EventQueue::default()), + pending_events, request_id_to_method_map: Mutex::new(HashMap::new()), lsps0_message_handler, + lsps2_message_handler, provider_config, channel_manager, chain_source, @@ -130,20 +201,191 @@ where { } } - /// Blocks until next event is ready and returns it + /// Blocks until next event is ready and returns it. /// - /// Typically you would spawn a thread or task that calls this in a loop + /// Typically you would spawn a thread or task that calls this in a loop. pub fn wait_next_event(&self) -> Event { self.pending_events.wait_next_event() } - /// Returns and clears all events without blocking + /// Returns and clears all events without blocking. /// - /// Typically you would spawn a thread or task that calls this in a loop + /// 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 + pub fn set_peer_manager( + &self, peer_manager: Arc>, + ) { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.set_peer_manager(peer_manager); + } + } + + /// 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 jit_channel_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 provide fee parameters to a client requesting a JIT Channel. + /// + /// Should be called in response to receiving a [`LSPS2Event::GetInfo`] event. + /// + /// [`LSPS2Event::GetInfo`]: crate::jit_channel::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::jit_channel::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::jit_channel::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::jit_channel::LSPS2Event::OpenChannel + pub fn htlc_intercepted( + &self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64, + 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, + inbound_amount_msat, + 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> { @@ -154,6 +396,14 @@ where { LSPSMessage::LSPS0(msg) => { self.lsps0_message_handler.handle_message(msg, sender_node_id)?; } + 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(()) } @@ -165,16 +415,22 @@ where { } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > CustomMessageReader for LiquidityManager + > CustomMessageReader + for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -183,32 +439,42 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { type CustomMessage = RawLSPSMessage; - fn read( + fn read( &self, message_type: u16, buffer: &mut RD, ) -> Result, lightning::ln::msgs::DecodeError> { match message_type { - LSPS_MESSAGE_TYPE => Ok(Some(RawLSPSMessage::read(buffer)?)), + LSPS_MESSAGE_TYPE_ID => Ok(Some(RawLSPSMessage::read(buffer)?)), _ => Ok(None), } } } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > CustomMessageHandler for LiquidityManager + > CustomMessageHandler + for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -217,15 +483,22 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { fn handle_custom_message( &self, msg: Self::CustomMessage, sender_node_id: &PublicKey, ) -> Result<(), lightning::ln::msgs::LightningError> { - let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); + 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 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); @@ -274,16 +547,21 @@ where } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > Listen for LiquidityManager + > Listen for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -292,6 +570,10 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { @@ -329,16 +611,21 @@ where } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > Confirm for LiquidityManager + > Confirm for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -347,6 +634,10 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { diff --git a/src/transport/mod.rs b/src/transport/mod.rs index fef2f5f..00185eb 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 u16 { - LSPS_MESSAGE_TYPE + LSPS_MESSAGE_TYPE_ID } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RequestId(pub String); #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -46,7 +55,6 @@ pub struct ResponseError { } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] -#[serde(default)] pub struct ListProtocolsRequest {} #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -86,6 +94,7 @@ impl TryFrom for LSPS0Message { match message { LSPSMessage::Invalid => Err(()), LSPSMessage::LSPS0(message) => Ok(message), + LSPSMessage::LSPS2(_) => Err(()), } } } @@ -100,6 +109,7 @@ impl From for LSPSMessage { pub enum LSPSMessage { Invalid, LSPS0(LSPS0Message), + LSPS2(LSPS2Message), } impl LSPSMessage { @@ -116,6 +126,9 @@ impl LSPSMessage { LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => { Some((request_id.0.clone(), request.method().to_string())) } + LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => { + Some((request_id.0.clone(), request.method().to_string())) + } _ => None, } } @@ -154,6 +167,43 @@ impl Serialize for LSPSMessage { } } } + LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + jsonrpc_object.serialize_field(JSONRPC_METHOD_FIELD_KEY, request.method())?; + + match request { + LSPS2Request::GetVersions(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + } + LSPS2Request::GetInfo(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + } + LSPS2Request::Buy(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + } + } + } + LSPSMessage::LSPS2(LSPS2Message::Response(request_id, response)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + + match response { + LSPS2Response::GetVersions(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + } + LSPS2Response::GetInfo(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + } + LSPS2Response::GetInfoError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + } + LSPS2Response::Buy(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + } + LSPS2Response::BuyError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + } + } + } LSPSMessage::Invalid => { let error = ResponseError { code: JSONRPC_INVALID_MESSAGE_ERROR_CODE, @@ -224,6 +274,30 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { LSPS0Request::ListProtocols(ListProtocolsRequest {}), ))) } + LSPS2_GET_VERSIONS_METHOD_NAME => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( + RequestId(id), + LSPS2Request::GetVersions(request), + ))) + } + LSPS2_GET_INFO_METHOD_NAME => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( + RequestId(id), + LSPS2Request::GetInfo(request), + ))) + } + LSPS2_BUY_METHOD_NAME => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( + RequestId(id), + LSPS2Request::Buy(request), + ))) + } _ => Err(de::Error::custom(format!( "Received request with unknown method: {}", method @@ -248,6 +322,52 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) } } + LSPS2_GET_VERSIONS_METHOD_NAME => { + if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + RequestId(id), + LSPS2Response::GetVersions(response), + ))) + } else { + Err(de::Error::custom("Received invalid lsps2.get_versions response.")) + } + } + LSPS2_GET_INFO_METHOD_NAME => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + RequestId(id), + LSPS2Response::GetInfoError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + RequestId(id), + LSPS2Response::GetInfo(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + } + LSPS2_BUY_METHOD_NAME => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + RequestId(id), + LSPS2Response::BuyError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + RequestId(id), + LSPS2Response::Buy(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + } _ => Err(de::Error::custom(format!( "Received response for an unknown request method: {}", method diff --git a/src/utils.rs b/src/utils.rs index 067ce0b..c5969db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,6 +4,39 @@ use std::{fmt::Write, ops::Deref}; use crate::transport::msgs::RequestId; +/// Maximum transaction index that can be used in a `short_channel_id`. +/// This value is based on the 3-bytes available for tx index. +pub const MAX_SCID_TX_INDEX: u64 = 0x00ffffff; + +/// Maximum vout index that can be used in a `short_channel_id`. This +/// value is based on the 2-bytes available for the vout index. +pub const MAX_SCID_VOUT_INDEX: u64 = 0xffff; + +/// Extracts the block height (most significant 3-bytes) from the `short_channel_id`. +pub fn block_from_scid(short_channel_id: &u64) -> u32 { + (short_channel_id >> 40) as u32 +} + +/// Extracts the tx index (bytes [2..4]) from the `short_channel_id`. +pub fn tx_index_from_scid(short_channel_id: &u64) -> u32 { + ((short_channel_id >> 16) & MAX_SCID_TX_INDEX) as u32 +} + +/// Extracts the vout (bytes [0..2]) from the `short_channel_id`. +pub fn vout_from_scid(short_channel_id: &u64) -> u16 { + ((short_channel_id) & MAX_SCID_VOUT_INDEX) as u16 +} + +pub fn scid_from_human_readable_string(human_readable_scid: &str) -> Result { + let mut parts = human_readable_scid.split('x'); + + let block: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?; + let tx_index: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?; + let vout_index: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?; + + Ok((block << 40) | (tx_index << 16) | vout_index) +} + pub(crate) fn generate_request_id(entropy_source: &ES) -> RequestId where ES::Target: EntropySource, @@ -67,3 +100,23 @@ pub fn parse_pubkey(pubkey_str: &str) -> Result { Ok(pubkey.unwrap()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_human_readable_scid_correctly() { + let block = 140; + let tx_index = 123; + let vout = 22; + + let human_readable_scid = format!("{}x{}x{}", block, tx_index, vout); + + let scid = scid_from_human_readable_string(&human_readable_scid).unwrap(); + + assert_eq!(block_from_scid(&scid), block); + assert_eq!(tx_index_from_scid(&scid), tx_index); + assert_eq!(vout_from_scid(&scid), vout); + } +}