diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 087bc1c17..96b03db21 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -115,6 +115,7 @@ enum NodeError { "MessageSigningFailed", "TxSyncFailed", "GossipUpdateFailed", + "LiquidityRequestFailed", "InvalidAddress", "InvalidSocketAddress", "InvalidPublicKey", diff --git a/src/error.rs b/src/error.rs index 115b4c570..ca1b6fc26 100644 --- a/src/error.rs +++ b/src/error.rs @@ -37,6 +37,8 @@ pub enum Error { TxSyncFailed, /// A gossip updating operation failed. GossipUpdateFailed, + /// A liquidity request operation failed. + LiquidityRequestFailed, /// The given address is invalid. InvalidAddress, /// The given network address is invalid. @@ -91,6 +93,7 @@ impl fmt::Display for Error { Self::MessageSigningFailed => write!(f, "Failed to sign given message."), Self::TxSyncFailed => write!(f, "Failed to sync transactions."), Self::GossipUpdateFailed => write!(f, "Failed to update gossip data."), + Self::LiquidityRequestFailed => write!(f, "Failed to request inbound liquidity."), Self::InvalidAddress => write!(f, "The given address is invalid."), Self::InvalidSocketAddress => write!(f, "The given network address is invalid."), Self::InvalidPublicKey => write!(f, "The given public key is invalid."), diff --git a/src/liquidity.rs b/src/liquidity.rs index 2cbcc15cd..73479768a 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -1,24 +1,32 @@ -use crate::logger::{log_error, Logger}; +use crate::logger::{log_debug, log_error, log_info, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager}; -use crate::Config; +use crate::{Config, Error}; +use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; use lightning::ln::features::{InitFeatures, NodeFeatures}; use lightning::ln::msgs::SocketAddress; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; +use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning::util::persist::KVStore; +use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::msgs::RawLSPSMessage; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::utils::compute_opening_fee; -use bitcoin::secp256k1::PublicKey; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; +use rand::Rng; + use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex}; +use std::time::Duration; pub(crate) enum LiquiditySource where @@ -191,6 +199,162 @@ where }, } } + + pub(crate) async fn lsps2_receive_to_jit_channel( + &self, amount_msat: Option, description: &str, expiry_secs: u32, + ) -> Result { + match self { + Self::None => Err(Error::LiquiditySourceUnavailable), + Self::LSPS2 { + node_id, + token, + pending_fee_requests, + pending_buy_requests, + channel_manager, + keys_manager, + liquidity_manager, + config, + logger, + .. + } => { + let user_channel_id: u128 = rand::thread_rng().gen::(); + + let (fee_request_sender, fee_request_receiver) = oneshot::channel(); + pending_fee_requests.lock().unwrap().insert(user_channel_id, fee_request_sender); + + let client_handler = liquidity_manager.lsps2_client_handler().ok_or_else(|| { + log_error!(logger, "Liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + client_handler.create_invoice( + *node_id, + amount_msat, + token.clone(), + user_channel_id, + ); + + let fee_response = fee_request_receiver.await.map_err(|e| { + log_error!(logger, "Failed to handle response from liquidity service: {:?}", e); + Error::LiquidityRequestFailed + })?; + debug_assert_eq!(fee_response.user_channel_id, user_channel_id); + + if let Some(amount_msat) = amount_msat { + if amount_msat < fee_response.min_payment_size_msat + || amount_msat > fee_response.max_payment_size_msat + { + log_error!(logger, "Failed to request inbound JIT channel as the payment of {}msat doesn't meet LSP limits (min: {}msat, max: {}msat)", amount_msat, fee_response.min_payment_size_msat, fee_response.max_payment_size_msat); + return Err(Error::LiquidityRequestFailed); + } + } + + // If it's variable amount, we pick the cheapest opening fee with a dummy value. + let fee_computation_amount = amount_msat.unwrap_or(1_000_000); + let (min_opening_fee_msat, min_opening_params) = fee_response + .opening_fee_params_menu + .iter() + .flat_map(|params| { + if let Some(fee) = compute_opening_fee( + fee_computation_amount, + params.min_fee_msat, + params.proportional as u64, + ) { + Some((fee, params)) + } else { + None + } + }) + .min_by_key(|p| p.0) + .ok_or_else(|| { + log_error!(logger, "Failed to handle response from liquidity service",); + Error::LiquidityRequestFailed + })?; + + log_debug!( + logger, + "Choosing cheapest liquidity offer, will pay {}msat in LSP fees", + min_opening_fee_msat + ); + + let (buy_request_sender, buy_request_receiver) = oneshot::channel(); + pending_buy_requests.lock().unwrap().insert(user_channel_id, buy_request_sender); + + client_handler + .opening_fee_params_selected( + *node_id, + fee_response.jit_channel_id, + min_opening_params.clone(), + ) + .map_err(|e| { + log_error!( + logger, + "Failed to send buy request to liquidity service: {:?}", + e + ); + Error::LiquidityRequestFailed + })?; + + let buy_response = buy_request_receiver.await.map_err(|e| { + log_error!(logger, "Failed to handle response from liquidity service: {:?}", e); + Error::LiquidityRequestFailed + })?; + debug_assert_eq!(buy_response.user_channel_id, user_channel_id); + + // TODO: register in payment store + // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. + let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; + let (payment_hash, payment_secret) = channel_manager + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .map_err(|e| { + log_error!(logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?; + + let route_hint = RouteHint(vec![RouteHintHop { + src_node_id: *node_id, + short_channel_id: buy_response.intercept_scid, + fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, + cltv_expiry_delta: buy_response.cltv_expiry_delta as u16, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }]); + + let payment_hash = sha256::Hash::from_slice(&payment_hash.0).map_err(|e| { + log_error!(logger, "Invalid payment hash: {:?}", e); + Error::InvoiceCreationFailed + })?; + + let currency = config.network.into(); + let mut invoice_builder = InvoiceBuilder::new(currency) + .description(description.to_string()) + .payment_hash(payment_hash) + .payment_secret(payment_secret) + .current_timestamp() + .min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into()) + .expiry_time(Duration::from_secs(expiry_secs.into())) + .private_route(route_hint); + + if let Some(amount_msat) = amount_msat { + invoice_builder = + invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp(); + } + + let invoice = invoice_builder + .build_signed(|hash| { + Secp256k1::new() + .sign_ecdsa_recoverable(hash, &keys_manager.get_node_secret_key()) + }) + .map_err(|e| { + log_error!(logger, "Failed to build and sign invoice: {}", e); + Error::InvoiceCreationFailed + })?; + + log_info!(logger, "JIT-channel invoice created: {}", invoice); + Ok(invoice) + } + } + } } impl CustomMessageReader for LiquiditySource diff --git a/src/types.rs b/src/types.rs index 531fa33aa..3a357719f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -186,6 +186,31 @@ impl From for bitcoin::Network { } } +impl TryFrom for Network { + type Error = (); + + fn try_from(network: lightning_invoice::Currency) -> Result { + match network { + lightning_invoice::Currency::Bitcoin => Ok(Self::Bitcoin), + lightning_invoice::Currency::BitcoinTestnet => Ok(Self::Testnet), + lightning_invoice::Currency::Signet => Ok(Self::Signet), + lightning_invoice::Currency::Regtest => Ok(Self::Regtest), + _ => Err(()), + } + } +} + +impl From for lightning_invoice::Currency { + fn from(network: Network) -> Self { + match network { + Network::Bitcoin => lightning_invoice::Currency::Bitcoin, + Network::Testnet => lightning_invoice::Currency::BitcoinTestnet, + Network::Signet => lightning_invoice::Currency::Signet, + Network::Regtest => lightning_invoice::Currency::Regtest, + } + } +} + impl fmt::Display for Network { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { bitcoin::Network::from(*self).fmt(f) diff --git a/src/wallet.rs b/src/wallet.rs index d3e6c0a47..5caa67264 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -23,7 +23,7 @@ use bitcoin::bech32::u5; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, Signing}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; use std::ops::Deref; @@ -299,6 +299,10 @@ where .or(Err(Error::MessageSigningFailed)) } + pub fn get_node_secret_key(&self) -> SecretKey { + self.inner.get_node_secret_key() + } + pub fn verify_signature(&self, msg: &[u8], sig: &str, pkey: &PublicKey) -> bool { message_signing::verify(msg, sig, pkey) }