From 8f48dc626308d51344cfa3545f163437403185ef Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 3 Jan 2025 11:04:20 +0100 Subject: [PATCH 01/13] Update BDK to 1.0.0 and LDK to 0.1.0 --- Cargo.toml | 58 +++++++++++++++++++---------------- bindings/ldk_node.udl | 4 ++- src/balance.rs | 3 +- src/builder.rs | 17 +++++++---- src/chain/bitcoind_rpc.rs | 5 +-- src/chain/mod.rs | 16 +++------- src/event.rs | 23 ++++++++++++-- src/io/test_utils.rs | 3 +- src/lib.rs | 3 +- src/message_handler.rs | 11 ++++--- src/payment/bolt11.rs | 62 +++++++++++--------------------------- src/payment/bolt12.rs | 4 +-- src/payment/spontaneous.rs | 5 +-- src/payment/store.rs | 3 +- src/types.rs | 2 ++ src/uniffi_types.rs | 5 +-- src/wallet/mod.rs | 41 +++++++------------------ tests/common/mod.rs | 4 ++- 18 files changed, 127 insertions(+), 142 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f22092cb5..61d2685ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,37 +28,42 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.0.125", features = ["std"] } -lightning-invoice = { version = "0.32.0" } -lightning-net-tokio = { version = "0.0.125" } -lightning-persister = { version = "0.0.125" } -lightning-background-processor = { version = "0.0.125", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.0.125" } -lightning-block-sync = { version = "0.0.125", features = ["rpc-client", "tokio"] } -lightning-transaction-sync = { version = "0.0.125", features = ["esplora-async-https", "time"] } -lightning-liquidity = { version = "0.1.0-alpha.6", features = ["std"] } - -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] } -#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["futures"] } -#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["esplora-async"] } -#lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity", branch="main", features = ["std"] } +lightning = { version = "0.1.0", features = ["std"] } +lightning-types = { version = "0.2.0" } +lightning-invoice = { version = "0.33.0", features = ["std"] } +lightning-net-tokio = { version = "0.1.0" } +lightning-persister = { version = "0.1.0" } +lightning-background-processor = { version = "0.1.0", features = ["futures"] } +lightning-rapid-gossip-sync = { version = "0.1.0" } +lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "tokio"] } +lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time"] } +lightning-liquidity = { version = "0.1.0", features = ["std"] } + +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } +#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } +#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["futures"] } +#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rpc-client", "tokio"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "time"] } +#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } -#lightning-invoice = { path = "../rust-lightning/lightning-invoice" } +#lightning-types = { path = "../rust-lightning/lightning-types" } +#lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } #lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } #lightning-persister = { path = "../rust-lightning/lightning-persister" } #lightning-background-processor = { path = "../rust-lightning/lightning-background-processor", features = ["futures"] } #lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async"] } -#lightning-liquidity = { path = "../lightning-liquidity", features = ["std"] } +#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rpc-client", "tokio"] } +#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "time"] } +#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } -bdk_chain = { version = "=0.19.0", default-features = false, features = ["std"] } -bdk_esplora = { version = "=0.18.0", default-features = false, features = ["async-https-rustls"]} -bdk_wallet = { version = "=1.0.0-beta.4", default-features = false, features = ["std", "keys-bip39"]} +bdk_chain = { version = "0.21.1", default-features = false, features = ["std"] } +bdk_esplora = { version = "0.20.1", default-features = false, features = ["async-https-rustls", "tokio"]} +bdk_wallet = { version = "1.0.0", default-features = false, features = ["std", "keys-bip39"]} reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } rusqlite = { version = "0.31.0", features = ["bundled"] } @@ -70,7 +75,7 @@ base64 = { version = "0.22.1", default-features = false, features = ["std"] } rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } -esplora-client = { version = "0.9", default-features = false } +esplora-client = { version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } libc = "0.2" uniffi = { version = "0.27.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } @@ -83,8 +88,9 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.0.125", features = ["std", "_test_utils"] } +lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +#lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } electrum-client = { version = "0.21.0", default-features = true } bitcoincore-rpc = { version = "0.19.0", default-features = false } proptest = "1.0.0" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index bc99c1783..5ea0e4173 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -280,7 +280,8 @@ interface Event { PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason); PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence custom_records); PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence custom_records); - PaymentForwarded(ChannelId prev_channel_id, ChannelId next_channel_id, UserChannelId? prev_user_channel_id, UserChannelId? next_user_channel_id, u64? total_fee_earned_msat, u64? skimmed_fee_msat, boolean claim_from_onchain_tx, u64? outbound_amount_forwarded_msat); + PaymentForwarded(ChannelId prev_channel_id, ChannelId next_channel_id, UserChannelId? + prev_user_channel_id, UserChannelId? next_user_channel_id, PublicKey? prev_node_id, PublicKey? next_node_id, u64? total_fee_earned_msat, u64? skimmed_fee_msat, boolean claim_from_onchain_tx, u64? outbound_amount_forwarded_msat); ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); @@ -296,6 +297,7 @@ enum PaymentFailureReason { "UnknownRequiredFeatures", "InvoiceRequestExpired", "InvoiceRequestRejected", + "BlindedPathCreationFailed", }; [Enum] diff --git a/src/balance.rs b/src/balance.rs index c43386d80..4d75c6c8e 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -10,9 +10,10 @@ use crate::sweep::value_from_descriptor; use lightning::chain::channelmonitor::Balance as LdkBalance; use lightning::chain::channelmonitor::BalanceSource; use lightning::ln::types::ChannelId; -use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::util::sweep::{OutputSpendStatus, TrackedSpendableOutput}; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Txid}; diff --git a/src/builder.rs b/src/builder.rs index fac2ae0c5..d4987d805 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -907,7 +907,7 @@ fn build_with_store_internal( )); // Read ChannelMonitor state from store - let mut channel_monitors = match read_channel_monitors( + let channel_monitors = match read_channel_monitors( Arc::clone(&kv_store), Arc::clone(&keys_manager), Arc::clone(&keys_manager), @@ -936,6 +936,9 @@ fn build_with_store_internal( 100; } + let message_router = + Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); + // Initialize the ChannelManager let channel_manager = { if let Ok(res) = kv_store.read( @@ -945,7 +948,7 @@ fn build_with_store_internal( ) { let mut reader = Cursor::new(res); let channel_monitor_references = - channel_monitors.iter_mut().map(|(_, chanmon)| chanmon).collect(); + channel_monitors.iter().map(|(_, chanmon)| chanmon).collect(); let read_args = ChannelManagerReadArgs::new( Arc::clone(&keys_manager), Arc::clone(&keys_manager), @@ -954,6 +957,7 @@ fn build_with_store_internal( Arc::clone(&chain_monitor), Arc::clone(&tx_broadcaster), Arc::clone(&router), + Arc::clone(&message_router), Arc::clone(&logger), user_config, channel_monitor_references, @@ -978,6 +982,7 @@ fn build_with_store_internal( Arc::clone(&chain_monitor), Arc::clone(&tx_broadcaster), Arc::clone(&router), + Arc::clone(&message_router), Arc::clone(&logger), Arc::clone(&keys_manager), Arc::clone(&keys_manager), @@ -1000,18 +1005,17 @@ fn build_with_store_internal( })?; } - let message_router = MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager)); - // Initialize the PeerManager let onion_messenger: Arc = Arc::new(OnionMessenger::new( Arc::clone(&keys_manager), Arc::clone(&keys_manager), Arc::clone(&logger), Arc::clone(&channel_manager), - Arc::new(message_router), + message_router, Arc::clone(&channel_manager), IgnoringMessageHandler {}, IgnoringMessageHandler {}, + IgnoringMessageHandler {}, )); let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); @@ -1055,7 +1059,8 @@ fn build_with_store_internal( let liquidity_source = liquidity_source_config.as_ref().and_then(|lsc| { lsc.lsps2_service.as_ref().map(|(address, node_id, token)| { let lsps2_client_config = Some(LSPS2ClientConfig {}); - let liquidity_client_config = Some(LiquidityClientConfig { lsps2_client_config }); + let liquidity_client_config = + Some(LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config }); let liquidity_manager = Arc::new(LiquidityManager::new( Arc::clone(&keys_manager), Arc::clone(&channel_manager), diff --git a/src/chain/bitcoind_rpc.rs b/src/chain/bitcoind_rpc.rs index 38bc4f2ee..3a3073378 100644 --- a/src/chain/bitcoind_rpc.rs +++ b/src/chain/bitcoind_rpc.rs @@ -38,10 +38,7 @@ impl BitcoindRpcClient { let rpc_credentials = BASE64_STANDARD.encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone())); - let rpc_client = Arc::new( - RpcClient::new(&rpc_credentials, http_endpoint) - .expect("RpcClient::new is actually infallible"), - ); + let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, http_endpoint)); let latest_mempool_timestamp = AtomicU64::new(0); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 6c89476d2..58d8a6417 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -787,18 +787,12 @@ impl ChainSource { for target in confirmation_targets { let num_blocks = get_num_block_defaults_for_target(target); + // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it + // yields less than that. This is mostly necessary to continue on + // `signet`/`regtest` where we might not get estimates (or bogus values). let converted_estimate_sat_vb = - esplora_client::convert_fee_rate(num_blocks, estimates.clone()).map_err( - |e| { - log_error!( - logger, - "Failed to convert fee rate estimates for {:?}: {}", - target, - e - ); - Error::FeerateEstimationUpdateFailed - }, - )?; + esplora_client::convert_fee_rate(num_blocks, estimates.clone()) + .map_or(1.0, |converted| converted.max(1.0)); let fee_rate = FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); diff --git a/src/event.rs b/src/event.rs index 5f5812cdb..1de77e937 100644 --- a/src/event.rs +++ b/src/event.rs @@ -32,11 +32,12 @@ use lightning::events::{Event as LdkEvent, PaymentFailureReason}; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; use lightning::ln::types::ChannelId; -use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::routing::gossip::NodeId; use lightning::util::errors::APIError; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + use lightning_liquidity::lsps2::utils::compute_opening_fee; use bitcoin::blockdata::locktime::absolute::LockTime; @@ -120,6 +121,16 @@ pub enum Event { /// This will be `None` if the payment was settled via an on-chain transaction. See the /// caveat described for the `total_fee_earned_msat` field. next_user_channel_id: Option, + /// The node id of the previous node. + /// + /// This is only `None` for HTLCs received prior to LDK Node v0.5 or for events serialized by + /// versions prior to v0.5. + prev_node_id: Option, + /// The node id of the next node. + /// + /// This is only `None` for HTLCs received prior to LDK Node v0.5 or for events serialized by + /// versions prior to v0.5. + next_node_id: Option, /// The total fee, in milli-satoshis, which was earned as a result of the payment. /// /// Note that if we force-closed the channel over which we forwarded an HTLC while the HTLC @@ -257,7 +268,9 @@ impl_writeable_tlv_based_enum!(Event, }, (7, PaymentForwarded) => { (0, prev_channel_id, required), + (1, prev_node_id, option), (2, next_channel_id, required), + (3, next_node_id, option), (4, prev_user_channel_id, option), (6, next_user_channel_id, option), (8, total_fee_earned_msat, option), @@ -550,6 +563,7 @@ where claim_deadline, onion_fields, counterparty_skimmed_fee_msat, + payment_id: _, } => { let payment_id = PaymentId(payment_hash.0); if let Some(info) = self.payment_store.get(&payment_id) { @@ -812,6 +826,7 @@ where htlcs: _, sender_intended_total_msat: _, onion_fields, + payment_id: _, } => { let payment_id = PaymentId(payment_hash.0); log_info!( @@ -1025,7 +1040,7 @@ where counterparty_node_id, funding_satoshis, channel_type, - push_msat: _, + channel_negotiation_type: _, is_announced: _, params: _, } => { @@ -1139,6 +1154,8 @@ where next_channel_id, prev_user_channel_id, next_user_channel_id, + prev_node_id, + next_node_id, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, @@ -1149,6 +1166,8 @@ where next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), prev_user_channel_id: prev_user_channel_id.map(UserChannelId), next_user_channel_id: next_user_channel_id.map(UserChannelId), + prev_node_id, + next_node_id, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index 98b33fa5f..aed03b6fc 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -11,7 +11,6 @@ use lightning::ln::functional_test_utils::{ }; use lightning::util::persist::{read_channel_monitors, KVStore, KVSTORE_NAMESPACE_KEY_MAX_LEN}; -use lightning::chain::channelmonitor::CLOSED_CHANNEL_UPDATE_ID; use lightning::events::ClosureReason; use lightning::util::test_utils; use lightning::{check_added_monitors, check_closed_broadcast, check_closed_event}; @@ -186,5 +185,5 @@ pub(crate) fn do_test_store(store_0: &K, store_1: &K) { check_added_monitors!(nodes[1], 1); // Make sure everything is persisted as expected after close. - check_persisted_data!(CLOSED_CHANNEL_UPDATE_ID); + check_persisted_data!(11); } diff --git a/src/lib.rs b/src/lib.rs index 363812292..140c6bb41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,7 @@ pub use bip39; pub use bitcoin; pub use lightning; pub use lightning_invoice; +pub use lightning_types; pub use vss_client; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; @@ -823,7 +824,6 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), - Arc::clone(&self.keys_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), @@ -841,7 +841,6 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), - Arc::clone(&self.keys_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), diff --git a/src/message_handler.rs b/src/message_handler.rs index 38999512e..cebd1ea07 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -7,11 +7,12 @@ use crate::liquidity::LiquiditySource; -use lightning::ln::features::{InitFeatures, NodeFeatures}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::util::logger::Logger; +use lightning_types::features::{InitFeatures, NodeFeatures}; + use lightning_liquidity::lsps0::ser::RawLSPSMessage; use bitcoin::secp256k1::PublicKey; @@ -63,7 +64,7 @@ where L::Target: Logger, { fn handle_custom_message( - &self, msg: Self::CustomMessage, sender_node_id: &PublicKey, + &self, msg: Self::CustomMessage, sender_node_id: PublicKey, ) -> Result<(), lightning::ln::msgs::LightningError> { match self { Self::Ignoring => Ok(()), // Should be unreachable!() as the reader will return `None` @@ -91,7 +92,7 @@ where } } - fn provided_init_features(&self, their_node_id: &PublicKey) -> InitFeatures { + fn provided_init_features(&self, their_node_id: PublicKey) -> InitFeatures { match self { Self::Ignoring => InitFeatures::empty(), Self::Liquidity { liquidity_source, .. } => { @@ -101,7 +102,7 @@ where } fn peer_connected( - &self, their_node_id: &PublicKey, msg: &lightning::ln::msgs::Init, inbound: bool, + &self, their_node_id: PublicKey, msg: &lightning::ln::msgs::Init, inbound: bool, ) -> Result<(), ()> { match self { Self::Ignoring => Ok(()), @@ -111,7 +112,7 @@ where } } - fn peer_disconnected(&self, their_node_id: &PublicKey) { + fn peer_disconnected(&self, their_node_id: PublicKey) { match self { Self::Ignoring => {}, Self::Liquidity { liquidity_source, .. } => { diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 708c127bd..d2a5840c0 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -20,24 +20,22 @@ use crate::payment::store::{ }; use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; -use crate::types::{ChannelManager, KeysManager}; +use crate::types::ChannelManager; -use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; -use lightning::ln::invoice_utils::{ - create_invoice_from_channelmanager_and_duration_since_epoch, - create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash, +use lightning::ln::bolt11_payment; +use lightning::ln::channelmanager::{ + Bolt11InvoiceParameters, PaymentId, RecipientOnionFields, Retry, RetryableSendFailure, }; -use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::routing::router::{PaymentParameters, RouteParameters}; -use lightning::ln::bolt11_payment; -use lightning_invoice::{Bolt11Invoice, Currency}; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use std::sync::{Arc, RwLock}; -use std::time::SystemTime; /// A payment handler allowing to create and pay [BOLT 11] invoices. /// @@ -49,7 +47,6 @@ pub struct Bolt11Payment { runtime: Arc>>>, channel_manager: Arc, connection_manager: Arc>>, - keys_manager: Arc, liquidity_source: Option>>>, payment_store: Arc>>, peer_store: Arc>>, @@ -62,7 +59,6 @@ impl Bolt11Payment { runtime: Arc>>>, channel_manager: Arc, connection_manager: Arc>>, - keys_manager: Arc, liquidity_source: Option>>>, payment_store: Arc>>, peer_store: Arc>>, config: Arc, @@ -72,7 +68,6 @@ impl Bolt11Payment { runtime, channel_manager, connection_manager, - keys_manager, liquidity_source, payment_store, peer_store, @@ -468,41 +463,20 @@ impl Bolt11Payment { &self, amount_msat: Option, description: &str, expiry_secs: u32, manual_claim_payment_hash: Option, ) -> Result { - let currency = Currency::from(self.config.network); - let keys_manager = Arc::clone(&self.keys_manager); - let duration = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("for the foreseeable future this shouldn't happen"); + let invoice_description = Bolt11InvoiceDescription::Direct( + Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?, + ); let invoice = { - let invoice_res = if let Some(payment_hash) = manual_claim_payment_hash { - create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash( - &self.channel_manager, - keys_manager, - Arc::clone(&self.logger), - currency, - amount_msat, - description.to_string(), - duration, - expiry_secs, - payment_hash, - None, - ) - } else { - create_invoice_from_channelmanager_and_duration_since_epoch( - &self.channel_manager, - keys_manager, - Arc::clone(&self.logger), - currency, - amount_msat, - description.to_string(), - duration, - expiry_secs, - None, - ) + let invoice_params = Bolt11InvoiceParameters { + amount_msats: amount_msat, + description: invoice_description, + invoice_expiry_delta_secs: Some(expiry_secs), + payment_hash: manual_claim_payment_hash, + ..Default::default() }; - match invoice_res { + match self.channel_manager.create_bolt11_invoice(invoice_params) { Ok(inv) => { log_info!(self.logger, "Invoice created: {}", inv); inv @@ -748,7 +722,7 @@ impl Bolt11Payment { Error::InvalidInvoice })? } else { - bolt11_payment::payment_parameters_from_zero_amount_invoice(&invoice, amount_msat).map_err(|_| { + bolt11_payment::payment_parameters_from_variable_amount_invoice(&invoice, amount_msat).map_err(|_| { log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); Error::InvalidInvoice })? diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 90024b7d3..c32b1a1a8 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -93,7 +93,7 @@ impl Bolt12Payment { max_total_routing_fee_msat, ) { Ok(()) => { - let payee_pubkey = offer.signing_pubkey(); + let payee_pubkey = offer.issuer_signing_pubkey(); log_info!( self.logger, "Initiated sending {}msat to {:?}", @@ -197,7 +197,7 @@ impl Bolt12Payment { max_total_routing_fee_msat, ) { Ok(()) => { - let payee_pubkey = offer.signing_pubkey(); + let payee_pubkey = offer.issuer_signing_pubkey(); log_info!( self.logger, "Initiated sending {}msat to {:?}", diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index a0f91134c..cff630781 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -17,10 +17,11 @@ use crate::payment::SendingParameters; use crate::types::{ChannelManager, CustomTlvRecord, KeysManager}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; -use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::routing::router::{PaymentParameters, RouteParameters}; use lightning::sign::EntropySource; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + use bitcoin::secp256k1::PublicKey; use std::sync::{Arc, RwLock}; @@ -122,7 +123,7 @@ impl SpontaneousPayment { None => RecipientOnionFields::spontaneous_empty(), }; - match self.channel_manager.send_spontaneous_payment_with_retry( + match self.channel_manager.send_spontaneous_payment( Some(payment_preimage), recipient_fields, PaymentId(payment_hash.0), diff --git a/src/payment/store.rs b/src/payment/store.rs index ee82544dc..fbeba669b 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -15,7 +15,6 @@ use crate::Error; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; -use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; use lightning::util::string::UntrustedString; @@ -24,6 +23,8 @@ use lightning::{ impl_writeable_tlv_based_enum, write_tlv_fields, }; +use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; + use std::collections::HashMap; use std::iter::FromIterator; use std::ops::Deref; diff --git a/src/types.rs b/src/types.rs index 4ec8e9fe8..fc1ea8d31 100644 --- a/src/types.rs +++ b/src/types.rs @@ -64,6 +64,7 @@ pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< Arc, Arc, Arc, + Arc, Arc, >; @@ -114,6 +115,7 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, IgnoringMessageHandler, IgnoringMessageHandler, + IgnoringMessageHandler, >; pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 9cb88597d..89e11efc5 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -16,17 +16,18 @@ pub use crate::config::{ pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; -pub use crate::types::CustomTlvRecord; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; -pub use lightning::ln::types::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; +pub use lightning::ln::types::ChannelId; pub use lightning::offers::invoice::Bolt12Invoice; pub use lightning::offers::offer::{Offer, OfferId}; pub use lightning::offers::refund::Refund; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::util::string::UntrustedString; +pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; + pub use lightning_invoice::Bolt11Invoice; pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index a4d4b066e..3533e6fef 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -16,18 +16,18 @@ use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::{BestBlock, Listen}; use lightning::events::bump_transaction::{Utxo, WalletSource}; +use lightning::ln::inbound_payment::ExpandedKey; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; use lightning::ln::script::ShutdownScript; use lightning::sign::{ - ChangeDestinationSource, EntropySource, InMemorySigner, KeyMaterial, KeysManager, NodeSigner, - OutputSpender, Recipient, SignerProvider, SpendableOutputDescriptor, + ChangeDestinationSource, EntropySource, InMemorySigner, KeysManager, NodeSigner, OutputSpender, + Recipient, SignerProvider, SpendableOutputDescriptor, }; use lightning::util::message_signing; use lightning_invoice::RawBolt11Invoice; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; -use bdk_chain::ChainPosition; use bdk_wallet::{Balance, KeychainKind, PersistedWallet, SignOptions, Update}; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; @@ -139,11 +139,7 @@ where let mut locked_wallet = self.inner.lock().unwrap(); let mut tx_builder = locked_wallet.build_tx(); - tx_builder - .add_recipient(output_script, amount) - .fee_rate(fee_rate) - .nlocktime(locktime) - .enable_rbf(); + tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).nlocktime(locktime); let mut psbt = match tx_builder.finish() { Ok(psbt) => { @@ -255,10 +251,7 @@ where OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => { let mut tx_builder = locked_wallet.build_tx(); let amount = Amount::from_sat(amount_sats); - tx_builder - .add_recipient(address.script_pubkey(), amount) - .fee_rate(fee_rate) - .enable_rbf(); + tx_builder.add_recipient(address.script_pubkey(), amount).fee_rate(fee_rate); tx_builder }, OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } => { @@ -277,8 +270,7 @@ where change_address_info.address.script_pubkey(), Amount::from_sat(cur_anchor_reserve_sats), ) - .fee_rate(fee_rate) - .enable_rbf(); + .fee_rate(fee_rate); match tmp_tx_builder.finish() { Ok(psbt) => psbt.unsigned_tx, Err(err) => { @@ -316,17 +308,12 @@ where let mut tx_builder = locked_wallet.build_tx(); tx_builder .add_recipient(address.script_pubkey(), estimated_spendable_amount) - .fee_absolute(estimated_tx_fee) - .enable_rbf(); + .fee_absolute(estimated_tx_fee); tx_builder }, OnchainSendAmount::AllDrainingReserve => { let mut tx_builder = locked_wallet.build_tx(); - tx_builder - .drain_wallet() - .drain_to(address.script_pubkey()) - .fee_rate(fee_rate) - .enable_rbf(); + tx_builder.drain_wallet().drain_to(address.script_pubkey()).fee_rate(fee_rate); tx_builder }, }; @@ -526,7 +513,7 @@ where let mut utxos = Vec::new(); let confirmed_txs: Vec = locked_wallet .transactions() - .filter(|t| matches!(t.chain_position, ChainPosition::Confirmed(_))) + .filter(|t| t.chain_position.is_confirmed()) .map(|t| t.tx_node.txid) .collect(); let unspent_confirmed_utxos = @@ -715,8 +702,8 @@ where self.inner.ecdh(recipient, other_key, tweak) } - fn get_inbound_payment_key_material(&self) -> KeyMaterial { - self.inner.get_inbound_payment_key_material() + fn get_inbound_payment_key(&self) -> ExpandedKey { + self.inner.get_inbound_payment_key() } fn sign_invoice( @@ -734,12 +721,6 @@ where ) -> Result { self.inner.sign_bolt12_invoice(invoice) } - - fn sign_bolt12_invoice_request( - &self, invoice_request: &lightning::offers::invoice_request::UnsignedInvoiceRequest, - ) -> Result { - self.inner.sign_bolt12_invoice_request(invoice_request) - } } impl OutputSpender for WalletKeysManager diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 65567cb64..f3cff00dc 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -17,10 +17,12 @@ use ldk_node::{ }; use lightning::ln::msgs::SocketAddress; -use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::routing::gossip::NodeAlias; use lightning::util::persist::KVStore; use lightning::util::test_utils::TestStore; + +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + use lightning_persister::fs_store::FilesystemStore; use bitcoin::hashes::sha256::Hash as Sha256; From c39bfba6d4db9cc5acea8e73aef174ac99941944 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 3 Jan 2025 15:07:28 +0100 Subject: [PATCH 02/13] Fail Unified QR URI parsing if any known param fails Previously, we decided to continue parsing any fields if we failed to parse a known (i.e., `lightning` or `lno`) parameter failed to parse. This however just hides the error and is a bit anti-idiomatic even though allowing to use *some* URI fields even in the face of incompatible formats for others. Here we therefore opt to fail parsing the URI if any field fails. --- src/payment/unified_qr.rs | 21 +++++++++------------ tests/integration_tests_rust.rs | 15 ++++++--------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 88d372456..029416b84 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -256,22 +256,19 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState { "lightning" => { let bolt11_value = String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?; - if let Ok(invoice) = bolt11_value.parse::() { - self.bolt11_invoice = Some(invoice); - Ok(bip21::de::ParamKind::Known) - } else { - Ok(bip21::de::ParamKind::Unknown) - } + let invoice = bolt11_value + .parse::() + .map_err(|_| Error::UriParameterParsingFailed)?; + self.bolt11_invoice = Some(invoice); + Ok(bip21::de::ParamKind::Known) }, "lno" => { let bolt12_value = String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?; - if let Ok(offer) = bolt12_value.parse::() { - self.bolt12_offer = Some(offer); - Ok(bip21::de::ParamKind::Known) - } else { - Ok(bip21::de::ParamKind::Unknown) - } + let offer = + bolt12_value.parse::().map_err(|_| Error::UriParameterParsingFailed)?; + self.bolt12_offer = Some(offer); + Ok(bip21::de::ParamKind::Known) }, _ => Ok(bip21::de::ParamKind::Unknown), } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 937af241b..2737a3d18 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -868,11 +868,10 @@ fn unified_qr_send_receive() { expect_payment_successful_event!(node_a, Some(offer_payment_id), None); - // Removed one character from the offer to fall back on to invoice. - // Still needs work - let uri_str_with_invalid_offer = &uri_str[..uri_str.len() - 1]; + // Cut off the BOLT12 part to fallback to BOLT11. + let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); let invoice_payment_id: PaymentId = - match node_a.unified_qr_payment().send(uri_str_with_invalid_offer) { + match node_a.unified_qr_payment().send(uri_str_without_offer) { Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected Bolt11 payment but got Bolt12"); }, @@ -893,11 +892,9 @@ fn unified_qr_send_receive() { let onchain_uqr_payment = node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); - // Removed a character from the offer, so it would move on to the other parameters. - let txid = match node_a - .unified_qr_payment() - .send(&onchain_uqr_payment.as_str()[..onchain_uqr_payment.len() - 1]) - { + // Cut off any lightning part to fallback to on-chain only. + let uri_str_without_lightning = onchain_uqr_payment.split("&lightning=").next().unwrap(); + let txid = match node_a.unified_qr_payment().send(&uri_str_without_lightning) { Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected on-chain payment but got Bolt12") }, From c6b5eafc7c9c56199b64c5bf0c3d860688b159ce Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 23 Jul 2024 10:50:38 +0200 Subject: [PATCH 03/13] Reorder LSPS2 API params .. to align with the rest of the APIs where we usually go `node_id`, `address`, etc. --- bindings/ldk_node.udl | 2 +- src/builder.rs | 16 ++++++++-------- src/liquidity.rs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 5ea0e4173..e0c7eda32 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -39,7 +39,7 @@ interface Builder { void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); - void set_liquidity_source_lsps2(SocketAddress address, PublicKey node_id, string? token); + void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); void set_network(Network network); [Throws=BuildError] diff --git a/src/builder.rs b/src/builder.rs index d4987d805..5bae712cc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -96,8 +96,8 @@ enum GossipSourceConfig { #[derive(Debug, Clone)] struct LiquiditySourceConfig { - // LSPS2 service's (address, node_id, token) - lsps2_service: Option<(SocketAddress, PublicKey, Option)>, + // LSPS2 service's (node_id, address, token) + lsps2_service: Option<(PublicKey, SocketAddress, Option)>, } impl Default for LiquiditySourceConfig { @@ -281,14 +281,14 @@ impl NodeBuilder { /// /// The given `token` will be used by the LSP to authenticate the user. pub fn set_liquidity_source_lsps2( - &mut self, address: SocketAddress, node_id: PublicKey, token: Option, + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, ) -> &mut Self { // Mark the LSP as trusted for 0conf self.config.trusted_peers_0conf.push(node_id.clone()); let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((address, node_id, token)); + liquidity_source_config.lsps2_service = Some((node_id, address, token)); self } @@ -600,9 +600,9 @@ impl ArcedNodeBuilder { /// /// The given `token` will be used by the LSP to authenticate the user. pub fn set_liquidity_source_lsps2( - &self, address: SocketAddress, node_id: PublicKey, token: Option, + &self, node_id: PublicKey, address: SocketAddress, token: Option, ) { - self.inner.write().unwrap().set_liquidity_source_lsps2(address, node_id, token); + self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } /// Sets the used storage directory path. @@ -1057,7 +1057,7 @@ fn build_with_store_internal( }; let liquidity_source = liquidity_source_config.as_ref().and_then(|lsc| { - lsc.lsps2_service.as_ref().map(|(address, node_id, token)| { + lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { let lsps2_client_config = Some(LSPS2ClientConfig {}); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config }); @@ -1070,8 +1070,8 @@ fn build_with_store_internal( liquidity_client_config, )); Arc::new(LiquiditySource::new_lsps2( - address.clone(), *node_id, + address.clone(), token.clone(), Arc::clone(&channel_manager), Arc::clone(&keys_manager), diff --git a/src/liquidity.rs b/src/liquidity.rs index 1dfb5453a..2373b7402 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -32,8 +32,8 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; struct LSPS2Service { - address: SocketAddress, node_id: PublicKey, + address: SocketAddress, token: Option, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, @@ -56,15 +56,15 @@ where L::Target: Logger, { pub(crate) fn new_lsps2( - address: SocketAddress, node_id: PublicKey, token: Option, + node_id: PublicKey, address: SocketAddress, token: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, config: Arc, logger: L, ) -> Self { let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); let lsps2_service = Some(LSPS2Service { - address, node_id, + address, token, pending_fee_requests, pending_buy_requests, From f430a9103f724f6e3c4e57cdc47670ee9f01a85b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 23 Jul 2024 10:58:21 +0200 Subject: [PATCH 04/13] Allow to set LSPS1 liquidity source We add support for LSPS1 liquidity sources. To this end we slightly refactor our logic to first create a `LiquiditySourceBuilder` that then can be used to `build()` the `LiquiditySource` with the configured services. --- src/builder.rs | 85 ++++++++++++++++++++++------------ src/liquidity.rs | 105 ++++++++++++++++++++++++++++++++++++++---- src/payment/bolt11.rs | 2 +- 3 files changed, 152 insertions(+), 40 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 5bae712cc..189aa0b62 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -15,7 +15,7 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; -use crate::liquidity::LiquiditySource; +use crate::liquidity::LiquiditySourceBuilder; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -51,9 +51,6 @@ use lightning::util::sweep::OutputSweeper; use lightning_persister::fs_store::FilesystemStore; -use lightning_liquidity::lsps2::client::LSPS2ClientConfig; -use lightning_liquidity::{LiquidityClientConfig, LiquidityManager}; - use bdk_wallet::template::Bip84; use bdk_wallet::KeychainKind; use bdk_wallet::Wallet as BdkWallet; @@ -96,13 +93,15 @@ enum GossipSourceConfig { #[derive(Debug, Clone)] struct LiquiditySourceConfig { + // LSPS1 service's (node_id, address, token) + lsps1_service: Option<(PublicKey, SocketAddress, Option)>, // LSPS2 service's (node_id, address, token) lsps2_service: Option<(PublicKey, SocketAddress, Option)>, } impl Default for LiquiditySourceConfig { fn default() -> Self { - Self { lsps2_service: None } + Self { lsps1_service: None, lsps2_service: None } } } @@ -273,7 +272,26 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its inbound liquidity from the given + /// Configures the [`Node`] instance to source inbound liquidity from the given + /// [LSPS1](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS1/README.md) + /// service. + /// + /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// + /// The given `token` will be used by the LSP to authenticate the user. + pub fn set_liquidity_source_lsps1( + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + ) -> &mut Self { + // Mark the LSP as trusted for 0conf + self.config.trusted_peers_0conf.push(node_id.clone()); + + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + liquidity_source_config.lsps1_service = Some((node_id, address, token)); + self + } + + /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. /// @@ -592,7 +610,20 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_rgs(rgs_server_url); } - /// Configures the [`Node`] instance to source its inbound liquidity from the given + /// Configures the [`Node`] instance to source inbound liquidity from the given + /// [LSPS1](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS1/README.md) + /// service. + /// + /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// + /// The given `token` will be used by the LSP to authenticate the user. + pub fn set_liquidity_source_lsps1( + &self, node_id: PublicKey, address: SocketAddress, token: Option, + ) { + self.inner.write().unwrap().set_liquidity_source_lsps1(node_id, address, token); + } + + /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. /// @@ -1056,30 +1087,24 @@ fn build_with_store_internal( }, }; - let liquidity_source = liquidity_source_config.as_ref().and_then(|lsc| { + let liquidity_source = liquidity_source_config.as_ref().map(|lsc| { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&chain_source), + Arc::clone(&config), + Arc::clone(&logger), + ); + + lsc.lsps1_service.as_ref().map(|(node_id, address, token)| { + liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone()) + }); + lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - let lsps2_client_config = Some(LSPS2ClientConfig {}); - let liquidity_client_config = - Some(LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config }); - let liquidity_manager = Arc::new(LiquidityManager::new( - Arc::clone(&keys_manager), - Arc::clone(&channel_manager), - Some(Arc::clone(&chain_source)), - None, - None, - liquidity_client_config, - )); - Arc::new(LiquiditySource::new_lsps2( - *node_id, - address.clone(), - token.clone(), - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - liquidity_manager, - Arc::clone(&config), - Arc::clone(&logger), - )) - }) + liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) + }); + + Arc::new(liquidity_source_builder.build()) }); let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { diff --git a/src/liquidity.rs b/src/liquidity.rs index 2373b7402..57b92f612 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -5,6 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use crate::chain::ChainSource; use crate::logger::{log_debug, log_error, log_info, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager}; use crate::{Config, Error}; @@ -15,9 +16,12 @@ use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; use lightning_liquidity::lsps2::utils::compute_opening_fee; +use lightning_liquidity::LiquidityClientConfig; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -31,47 +35,126 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; +struct LSPS1Service { + node_id: PublicKey, + address: SocketAddress, + token: Option, + client_config: LSPS1ClientConfig, +} + struct LSPS2Service { node_id: PublicKey, address: SocketAddress, token: Option, + client_config: LSPS2ClientConfig, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, } -pub(crate) struct LiquiditySource +pub(crate) struct LiquiditySourceBuilder where L::Target: Logger, { + lsps1_service: Option, lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, - liquidity_manager: Arc, + chain_source: Arc, config: Arc, logger: L, } -impl LiquiditySource +impl LiquiditySourceBuilder where L::Target: Logger, { - pub(crate) fn new_lsps2( - node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn new( channel_manager: Arc, keys_manager: Arc, - liquidity_manager: Arc, config: Arc, logger: L, + chain_source: Arc, config: Arc, logger: L, ) -> Self { + let lsps1_service = None; + let lsps2_service = None; + Self { + lsps1_service, + lsps2_service, + channel_manager, + keys_manager, + chain_source, + config, + logger, + } + } + + pub(crate) fn lsps1_service( + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + ) -> &mut Self { + // TODO: allow to set max_channel_fees_msat + let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + self.lsps1_service = Some(LSPS1Service { node_id, address, token, client_config }); + self + } + + pub(crate) fn lsps2_service( + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + ) -> &mut Self { + let client_config = LSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - let lsps2_service = Some(LSPS2Service { + self.lsps2_service = Some(LSPS2Service { node_id, address, token, + client_config, pending_fee_requests, pending_buy_requests, }); - Self { lsps2_service, channel_manager, keys_manager, liquidity_manager, config, logger } + self } + pub(crate) fn build(self) -> LiquiditySource { + let lsps1_client_config = self.lsps1_service.as_ref().map(|s| s.client_config.clone()); + let lsps2_client_config = self.lsps2_service.as_ref().map(|s| s.client_config.clone()); + let liquidity_client_config = + Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); + + let liquidity_manager = Arc::new(LiquidityManager::new( + Arc::clone(&self.keys_manager), + Arc::clone(&self.channel_manager), + Some(Arc::clone(&self.chain_source)), + None, + None, + liquidity_client_config, + )); + + LiquiditySource { + lsps1_service: self.lsps1_service, + lsps2_service: self.lsps2_service, + channel_manager: self.channel_manager, + keys_manager: self.keys_manager, + liquidity_manager, + config: self.config, + logger: self.logger, + } + } +} + +pub(crate) struct LiquiditySource +where + L::Target: Logger, +{ + lsps1_service: Option, + lsps2_service: Option, + channel_manager: Arc, + keys_manager: Arc, + liquidity_manager: Arc, + config: Arc, + logger: L, +} + +impl LiquiditySource +where + L::Target: Logger, +{ pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { let process_msgs_callback = move || peer_manager.process_events(); self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); @@ -81,7 +164,11 @@ where self.liquidity_manager.as_ref() } - pub(crate) fn get_liquidity_source_details(&self) -> Option<(PublicKey, SocketAddress)> { + pub(crate) fn get_lsps1_service_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_service.as_ref().map(|s| (s.node_id, s.address.clone())) + } + + pub(crate) fn get_lsps2_service_details(&self) -> Option<(PublicKey, SocketAddress)> { self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone())) } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index d2a5840c0..cd6540274 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -576,7 +576,7 @@ impl Bolt11Payment { self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let (node_id, address) = liquidity_source - .get_liquidity_source_details() + .get_lsps2_service_details() .ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); From 9c96f318e1c05141c082246394160212236305db Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Dec 2024 14:00:52 +0100 Subject: [PATCH 05/13] Add `create_order` logic We add the logic required to send `create_order` requests and check on their status. --- src/liquidity.rs | 369 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 368 insertions(+), 1 deletion(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 57b92f612..3ebe64633 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -17,6 +17,10 @@ use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps1::event::LSPS1ClientEvent; +use lightning_liquidity::lsps1::msgs::{ + ChannelInfo, LSPS1Options, OrderId, OrderParameters, PaymentInfo, +}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; @@ -40,6 +44,11 @@ struct LSPS1Service { address: SocketAddress, token: Option, client_config: LSPS1ClientConfig, + pending_opening_params_requests: + Mutex>>, + pending_create_order_requests: Mutex>>, + pending_check_order_status_requests: + Mutex>>, } struct LSPS2Service { @@ -90,7 +99,18 @@ where ) -> &mut Self { // TODO: allow to set max_channel_fees_msat let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; - self.lsps1_service = Some(LSPS1Service { node_id, address, token, client_config }); + let pending_opening_params_requests = Mutex::new(HashMap::new()); + let pending_create_order_requests = Mutex::new(HashMap::new()); + let pending_check_order_status_requests = Mutex::new(HashMap::new()); + self.lsps1_service = Some(LSPS1Service { + node_id, + address, + token, + client_config, + pending_opening_params_requests, + pending_create_order_requests, + pending_check_order_status_requests, + }); self } @@ -174,6 +194,175 @@ where pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { + Event::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + request_id, + counterparty_node_id, + supported_options, + }) => { + if let Some(lsps1_service) = self.lsps1_service.as_ref() { + if counterparty_node_id != lsps1_service.node_id { + debug_assert!( + false, + "Received response from unexpected LSP counterparty. This should never happen." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_service + .pending_opening_params_requests + .lock() + .unwrap() + .remove(&request_id) + { + let response = LSPS1OpeningParamsResponse { supported_options }; + + match sender.send(response) { + Ok(()) => (), + Err(e) => { + log_error!( + self.logger, + "Failed to handle response from liquidity service: {:?}", + e + ); + }, + } + } else { + debug_assert!( + false, + "Received response from liquidity service for unknown request." + ); + log_error!( + self.logger, + "Received response from liquidity service for unknown request." + ); + } + } else { + log_error!( + self.logger, + "Received unexpected LSPS1Client::SupportedOptionsReady event!" + ); + } + }, + Event::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) => { + if let Some(lsps1_service) = self.lsps1_service.as_ref() { + if counterparty_node_id != lsps1_service.node_id { + debug_assert!( + false, + "Received response from unexpected LSP counterparty. This should never happen." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_service + .pending_create_order_requests + .lock() + .unwrap() + .remove(&request_id) + { + let response = LSPS1OrderStatus { + order_id, + order_params: order, + payment_options: payment, + channel_state: channel, + }; + + match sender.send(response) { + Ok(()) => (), + Err(e) => { + log_error!( + self.logger, + "Failed to handle response from liquidity service: {:?}", + e + ); + }, + } + } else { + debug_assert!( + false, + "Received response from liquidity service for unknown request." + ); + log_error!( + self.logger, + "Received response from liquidity service for unknown request." + ); + } + } else { + log_error!(self.logger, "Received unexpected LSPS1Client::OrderCreated event!"); + } + }, + Event::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) => { + if let Some(lsps1_service) = self.lsps1_service.as_ref() { + if counterparty_node_id != lsps1_service.node_id { + debug_assert!( + false, + "Received response from unexpected LSP counterparty. This should never happen." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_service + .pending_check_order_status_requests + .lock() + .unwrap() + .remove(&request_id) + { + let response = LSPS1OrderStatus { + order_id, + order_params: order, + payment_options: payment, + channel_state: channel, + }; + + match sender.send(response) { + Ok(()) => (), + Err(e) => { + log_error!( + self.logger, + "Failed to handle response from liquidity service: {:?}", + e + ); + }, + } + } else { + debug_assert!( + false, + "Received response from liquidity service for unknown request." + ); + log_error!( + self.logger, + "Received response from liquidity service for unknown request." + ); + } + } else { + log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); + } + }, Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, @@ -282,6 +471,166 @@ where } } + pub(crate) async fn lsps1_request_opening_params( + &self, + ) -> Result { + let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { + log_error!(self.logger, "LSPS1 liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_opening_params_requests_lock = + lsps1_service.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_service.node_id); + pending_opening_params_requests_lock.insert(request_id, request_sender); + } + + tokio::time::timeout(Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), request_receiver) + .await + .map_err(|e| { + log_error!(self.logger, "Liquidity request timed out: {}", e); + Error::LiquidityRequestFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to handle response from liquidity service: {}", e); + Error::LiquidityRequestFailed + }) + } + + pub(crate) async fn lsps1_request_channel( + &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, + announce_channel: bool, refund_address: bitcoin::Address, + ) -> Result { + let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { + log_error!(self.logger, "LSPS1 liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + let lsp_limits = self.lsps1_request_opening_params().await?.supported_options; + let channel_size_sat = lsp_balance_sat + client_balance_sat; + + if channel_size_sat < lsp_limits.min_channel_balance_sat + || channel_size_sat > lsp_limits.max_channel_balance_sat + { + log_error!( + self.logger, + "Requested channel size doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).", + lsp_limits.min_channel_balance_sat, + lsp_limits.max_channel_balance_sat + ); + return Err(Error::LiquidityRequestFailed); + } + + if lsp_balance_sat < lsp_limits.min_initial_lsp_balance_sat + || lsp_balance_sat > lsp_limits.max_initial_lsp_balance_sat + { + log_error!( + self.logger, + "Requested LSP-side balance doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).", + lsp_limits.min_initial_lsp_balance_sat, + lsp_limits.max_initial_lsp_balance_sat + ); + return Err(Error::LiquidityRequestFailed); + } + + if client_balance_sat < lsp_limits.min_initial_client_balance_sat + || client_balance_sat > lsp_limits.max_initial_client_balance_sat + { + log_error!( + self.logger, + "Requested client-side balance doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).", + lsp_limits.min_initial_client_balance_sat, + lsp_limits.max_initial_client_balance_sat + ); + return Err(Error::LiquidityRequestFailed); + } + + let order_params = OrderParameters { + lsp_balance_sat, + client_balance_sat, + required_channel_confirmations: lsp_limits.min_required_channel_confirmations, + funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, + channel_expiry_blocks, + token: lsps1_service.token.clone(), + announce_channel, + }; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_create_order_requests_lock = + lsps1_service.pending_create_order_requests.lock().unwrap(); + let request_id = client_handler.create_order( + &lsps1_service.node_id, + order_params.clone(), + Some(refund_address), + ); + pending_create_order_requests_lock.insert(request_id, request_sender); + } + + let response = tokio::time::timeout( + Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), + request_receiver, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Liquidity request timed out: {}", e); + Error::LiquidityRequestFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to handle response from liquidity service: {}", e); + Error::LiquidityRequestFailed + })?; + + if response.order_params != order_params { + log_error!( + self.logger, + "Aborting LSPS1 request as LSP-provided parameters don't match our order. Expected: {:?}, Received: {:?}", order_params, response.order_params + ); + return Err(Error::LiquidityRequestFailed); + } + + Ok(response) + } + + pub(crate) async fn lsps1_check_order_status( + &self, order_id: OrderId, + ) -> Result { + let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { + log_error!(self.logger, "LSPS1 liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_check_order_status_requests_lock = + lsps1_service.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_service.node_id, order_id); + pending_check_order_status_requests_lock.insert(request_id, request_sender); + } + + let response = tokio::time::timeout( + Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), + request_receiver, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Liquidity request timed out: {}", e); + Error::LiquidityRequestFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to handle response from liquidity service: {}", e); + Error::LiquidityRequestFailed + })?; + + Ok(response) + } + pub(crate) async fn lsps2_receive_to_jit_channel( &self, amount_msat: u64, description: &str, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, @@ -515,6 +864,24 @@ where } } +#[derive(Debug, Clone)] +pub(crate) struct LSPS1OpeningParamsResponse { + supported_options: LSPS1Options, +} + +/// Represents the status of an LSPS1 channel request. +#[derive(Debug, Clone)] +pub struct LSPS1OrderStatus { + /// The id of the channel order. + pub order_id: OrderId, + /// The parameters of channel order. + pub order_params: OrderParameters, + /// Contains details about how to pay for the order. + pub payment_options: PaymentInfo, + /// Contains information about the channel state. + pub channel_state: Option, +} + #[derive(Debug, Clone)] pub(crate) struct LSPS2FeeResponse { opening_fee_params_menu: Vec, From 3368f4e49d184625ce11d389e8c4d0b0f03bc9d0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Dec 2024 14:02:26 +0100 Subject: [PATCH 06/13] Add LSPS1 API We add an `Lsps1Liquidity` API object, mirroring the approach we took with the `payment` APIs. --- src/lib.rs | 33 ++++++++++++- src/liquidity.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 140c6bb41..01e6fdb32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,7 @@ mod gossip; pub mod graph; mod hex_utils; pub mod io; -mod liquidity; +pub mod liquidity; mod logger; mod message_handler; pub mod payment; @@ -100,6 +100,7 @@ pub use bip39; pub use bitcoin; pub use lightning; pub use lightning_invoice; +pub use lightning_liquidity; pub use lightning_types; pub use vss_client; @@ -130,7 +131,7 @@ use event::{EventHandler, EventQueue}; use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; -use liquidity::LiquiditySource; +use liquidity::{LiquiditySource, Lsps1Liquidity}; use payment::store::PaymentStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, @@ -959,6 +960,34 @@ impl Node { )) } + /// Returns a liquidity handler allowing to request channels via the [LSPS1] protocol. + /// + /// [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 + #[cfg(not(feature = "uniffi"))] + pub fn lsps1_liquidity(&self) -> Lsps1Liquidity { + Lsps1Liquidity::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), + Arc::clone(&self.logger), + ) + } + + /// Returns a liquidity handler allowing to request channels via the [LSPS1] protocol. + /// + /// [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 + #[cfg(feature = "uniffi")] + pub fn lsps1_liquidity(&self) -> Arc { + Arc::new(Lsps1Liquidity::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), + Arc::clone(&self.logger), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() diff --git a/src/liquidity.rs b/src/liquidity.rs index 3ebe64633..ade2ddd66 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -5,9 +5,12 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +//! Objects related to liquidity management. + use crate::chain::ChainSource; -use crate::logger::{log_debug, log_error, log_info, Logger}; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager}; +use crate::connection::ConnectionManager; +use crate::logger::{log_debug, log_error, log_info, FilesystemLogger, Logger}; +use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{Config, Error}; use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; @@ -34,7 +37,7 @@ use tokio::sync::oneshot; use std::collections::HashMap; use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; @@ -892,3 +895,114 @@ pub(crate) struct LSPS2BuyResponse { intercept_scid: u64, cltv_expiry_delta: u32, } + +/// A liquidity handler allowing to request channels via the [LSPS1] protocol. +/// +/// Should be retrieved by calling [`Node::lsps1_liquidity`]. +/// +/// [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 +/// [`Node::lsps1_liquidity`]: crate::Node::lsps1_liquidity +#[derive(Clone)] +pub struct Lsps1Liquidity { + runtime: Arc>>>, + wallet: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, + logger: Arc, +} + +impl Lsps1Liquidity { + pub(crate) fn new( + runtime: Arc>>>, wallet: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, + logger: Arc, + ) -> Self { + Self { runtime, wallet, connection_manager, liquidity_source, logger } + } + + /// Connects to the configured LSP and places an order for an inbound channel. + /// + /// The channel will be opened after one of the returned payment options has successfully been + /// paid. + pub fn request_channel( + &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, + announce_channel: bool, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (lsp_node_id, lsp_address) = liquidity_source + .get_lsps1_service_details() + .ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); + + let con_node_id = lsp_node_id; + let con_addr = lsp_address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", lsp_node_id, lsp_address); + + let refund_address = self.wallet.get_new_address()?; + + let liquidity_source = Arc::clone(&liquidity_source); + let response = tokio::task::block_in_place(move || { + runtime.block_on(async move { + liquidity_source + .lsps1_request_channel( + lsp_balance_sat, + client_balance_sat, + channel_expiry_blocks, + announce_channel, + refund_address, + ) + .await + }) + })?; + + Ok(response) + } + + /// Connects to the configured LSP and checks for the status of a previously-placed order. + pub fn check_order_status(&self, order_id: OrderId) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (lsp_node_id, lsp_address) = liquidity_source + .get_lsps1_service_details() + .ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); + + let con_node_id = lsp_node_id; + let con_addr = lsp_address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + let liquidity_source = Arc::clone(&liquidity_source); + let response = tokio::task::block_in_place(move || { + runtime + .block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await }) + })?; + + Ok(response) + } +} From 5e13aa579e057420f3d1e8aef9e094eb09f11964 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Dec 2024 15:37:56 +0100 Subject: [PATCH 07/13] Add Uniffi bindings for LSPS1 API --- bindings/ldk_node.udl | 73 +++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 6 ++++ src/uniffi_types.rs | 47 +++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e0c7eda32..b751eac16 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -39,6 +39,7 @@ interface Builder { void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); + void set_liquidity_source_lsps1(PublicKey node_id, SocketAddress address, string? token); void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); void set_network(Network network); @@ -78,6 +79,7 @@ interface Node { SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); UnifiedQrPayment unified_qr_payment(); + Lsps1Liquidity lsps1_liquidity(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -173,6 +175,13 @@ interface UnifiedQrPayment { QrPaymentResult send([ByRef]string uri_str); }; +interface Lsps1Liquidity { + [Throws=NodeError] + LSPS1OrderStatus request_channel(u64 lsp_balance_sat, u64 client_balance_sat, u32 channel_expiry_blocks, boolean announce_channel); + [Throws=NodeError] + LSPS1OrderStatus check_order_status(OrderId order_id); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -220,6 +229,8 @@ enum NodeError { "InvalidUri", "InvalidQuantity", "InvalidNodeAlias", + "InvalidDateTime", + "InvalidFeeRate", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -372,6 +383,59 @@ dictionary CustomTlvRecord { sequence value; }; +dictionary LSPS1OrderStatus { + OrderId order_id; + OrderParameters order_params; + PaymentInfo payment_options; + ChannelOrderInfo? channel_state; +}; + +dictionary OrderParameters { + u64 lsp_balance_sat; + u64 client_balance_sat; + u16 required_channel_confirmations; + u16 funding_confirms_within_blocks; + u32 channel_expiry_blocks; + string? token; + boolean announce_channel; +}; + +dictionary PaymentInfo { + Bolt11PaymentInfo? bolt11; + OnchainPaymentInfo? onchain; +}; + +dictionary Bolt11PaymentInfo { + PaymentState state; + DateTime expires_at; + u64 fee_total_sat; + u64 order_total_sat; + Bolt11Invoice invoice; +}; + +dictionary OnchainPaymentInfo { + PaymentState state; + DateTime expires_at; + u64 fee_total_sat; + u64 order_total_sat; + Address address; + u16? min_onchain_payment_confirmations; + FeeRate min_fee_for_0conf; + Address? refund_onchain_address; +}; + +dictionary ChannelOrderInfo { + DateTime funded_at; + OutPoint funding_outpoint; + DateTime expires_at; +}; + +enum PaymentState { + "ExpectPayment", + "Paid", + "Refunded", +}; + [Enum] interface MaxTotalRoutingFeeLimit { None (); @@ -624,3 +688,12 @@ typedef string UntrustedString; [Custom] typedef string NodeAlias; + +[Custom] +typedef string OrderId; + +[Custom] +typedef string DateTime; + +[Custom] +typedef string FeeRate; diff --git a/src/error.rs b/src/error.rs index ec1182c87..2cb71186d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -106,6 +106,10 @@ pub enum Error { InvalidQuantity, /// The given node alias is invalid. InvalidNodeAlias, + /// The given date time is invalid. + InvalidDateTime, + /// The given fee rate is invalid. + InvalidFeeRate, /// A payment with the given hash has already been initiated. DuplicatePayment, /// The provided offer was denonminated in an unsupported currency. @@ -172,6 +176,8 @@ impl fmt::Display for Error { Self::InvalidUri => write!(f, "The given URI is invalid."), Self::InvalidQuantity => write!(f, "The given quantity is invalid."), Self::InvalidNodeAlias => write!(f, "The given node alias is invalid."), + Self::InvalidDateTime => write!(f, "The given date time is invalid."), + Self::InvalidFeeRate => write!(f, "The given fee rate is invalid."), Self::DuplicatePayment => { write!(f, "A payment with the given hash has already been initiated.") }, diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 89e11efc5..6131f5032 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -14,6 +14,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; +pub use crate::liquidity::LSPS1OrderStatus; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; @@ -30,12 +31,19 @@ pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_invoice::Bolt11Invoice; -pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; +pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; +pub use lightning_liquidity::lsps1::msgs::{ + Bolt11PaymentInfo, OnchainPaymentInfo, OrderId, OrderParameters, PaymentInfo, PaymentState, +}; + +pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; pub use bip39::Mnemonic; pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; +pub type DateTime = chrono::DateTime; + use crate::UniffiCustomTypeConverter; use crate::builder::sanitize_alias; @@ -345,3 +353,40 @@ impl UniffiCustomTypeConverter for NodeAlias { obj.to_string() } } + +impl UniffiCustomTypeConverter for OrderId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Self(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.0 + } +} + +impl UniffiCustomTypeConverter for DateTime { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(DateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_rfc3339() + } +} + +/// FIXME TODO +impl UniffiCustomTypeConverter for FeeRate { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(FeeRate::from_str(&val).map_err(|_| Error::InvalidFeeRate)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} From a7bed0f88c38496f87f4f08509a15fd83ea0c415 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 11:55:05 +0100 Subject: [PATCH 08/13] Prefactor: Rename fields for clarity Previously, we named internal fields/APIs `lspsX_service` as us being the client was implied. Since we're about to also add service-side functionalities, such naming would start to get confusing. We hence rename them to follow a `lspsX_client` scheme, and will add the service-side APIs using the `service` terminology. --- src/builder.rs | 50 ++++++++++----- src/liquidity.rs | 140 +++++++++++++++++++++--------------------- src/payment/bolt11.rs | 5 +- 3 files changed, 105 insertions(+), 90 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 189aa0b62..8bfa57ade 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -91,18 +91,26 @@ enum GossipSourceConfig { RapidGossipSync(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] struct LiquiditySourceConfig { - // LSPS1 service's (node_id, address, token) - lsps1_service: Option<(PublicKey, SocketAddress, Option)>, - // LSPS2 service's (node_id, address, token) - lsps2_service: Option<(PublicKey, SocketAddress, Option)>, + // Act as an LSPS1 client connecting to the given service. + lsps1_client: Option, + // Act as an LSPS2 client connecting to the given service. + lsps2_client: Option, } -impl Default for LiquiditySourceConfig { - fn default() -> Self { - Self { lsps1_service: None, lsps2_service: None } - } +#[derive(Debug, Clone)] +struct LSPS1ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, +} + +#[derive(Debug, Clone)] +struct LSPS2ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, } /// An error encountered during building a [`Node`]. @@ -287,7 +295,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps1_service = Some((node_id, address, token)); + let lsps1_client_config = LSPS1ClientConfig { node_id, address, token }; + liquidity_source_config.lsps1_client = Some(lsps1_client_config); self } @@ -306,7 +315,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((node_id, address, token)); + let lsps2_client_config = LSPS2ClientConfig { node_id, address, token }; + liquidity_source_config.lsps2_client = Some(lsps2_client_config); self } @@ -955,7 +965,7 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); - if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() { // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll // check that they don't take too much before claiming. user_config.channel_config.accept_underpaying_htlcs = true; @@ -1096,12 +1106,20 @@ fn build_with_store_internal( Arc::clone(&logger), ); - lsc.lsps1_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone()) + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) }); - lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) }); Arc::new(liquidity_source_builder.build()) diff --git a/src/liquidity.rs b/src/liquidity.rs index ade2ddd66..cf38d711b 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -42,9 +42,9 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; -struct LSPS1Service { - node_id: PublicKey, - address: SocketAddress, +struct LSPS1Client{ + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, client_config: LSPS1ClientConfig, pending_opening_params_requests: @@ -54,9 +54,9 @@ struct LSPS1Service { Mutex>>, } -struct LSPS2Service { - node_id: PublicKey, - address: SocketAddress, +struct LSPS2Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, client_config: LSPS2ClientConfig, pending_fee_requests: Mutex>>, @@ -67,8 +67,8 @@ pub(crate) struct LiquiditySourceBuilder where L::Target: Logger, { - lsps1_service: Option, - lsps2_service: Option, + lsps1_client: Option, + lsps2_client: Option, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -84,11 +84,11 @@ where channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, ) -> Self { - let lsps1_service = None; - let lsps2_service = None; + let lsps1_client = None; + let lsps2_client = None; Self { - lsps1_service, - lsps2_service, + lsps1_client, + lsps2_client, channel_manager, keys_manager, chain_source, @@ -97,17 +97,17 @@ where } } - pub(crate) fn lsps1_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps1_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; let pending_opening_params_requests = Mutex::new(HashMap::new()); let pending_create_order_requests = Mutex::new(HashMap::new()); let pending_check_order_status_requests = Mutex::new(HashMap::new()); - self.lsps1_service = Some(LSPS1Service { - node_id, - address, + self.lsps1_client = Some(LSPS1Client { + lsp_node_id, + lsp_address, token, client_config, pending_opening_params_requests, @@ -117,15 +117,15 @@ where self } - pub(crate) fn lsps2_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps2_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { let client_config = LSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - self.lsps2_service = Some(LSPS2Service { - node_id, - address, + self.lsps2_client = Some(LSPS2Client { + lsp_node_id, + lsp_address, token, client_config, pending_fee_requests, @@ -135,8 +135,8 @@ where } pub(crate) fn build(self) -> LiquiditySource { - let lsps1_client_config = self.lsps1_service.as_ref().map(|s| s.client_config.clone()); - let lsps2_client_config = self.lsps2_service.as_ref().map(|s| s.client_config.clone()); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.client_config.clone()); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); @@ -150,8 +150,8 @@ where )); LiquiditySource { - lsps1_service: self.lsps1_service, - lsps2_service: self.lsps2_service, + lsps1_client: self.lsps1_client, + lsps2_client: self.lsps2_client, channel_manager: self.channel_manager, keys_manager: self.keys_manager, liquidity_manager, @@ -165,8 +165,8 @@ pub(crate) struct LiquiditySource where L::Target: Logger, { - lsps1_service: Option, - lsps2_service: Option, + lsps1_client: Option, + lsps2_client: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, @@ -187,12 +187,12 @@ where self.liquidity_manager.as_ref() } - pub(crate) fn get_lsps1_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps1_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } - pub(crate) fn get_lsps2_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps2_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } pub(crate) async fn handle_next_event(&self) { @@ -202,8 +202,8 @@ where counterparty_node_id, supported_options, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -215,7 +215,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_opening_params_requests .lock() .unwrap() @@ -258,8 +258,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -271,7 +271,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_create_order_requests .lock() .unwrap() @@ -316,8 +316,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -329,7 +329,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_check_order_status_requests .lock() .unwrap() @@ -371,8 +371,8 @@ where counterparty_node_id, opening_fee_params_menu, }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -385,7 +385,7 @@ where } if let Some(sender) = - lsps2_service.pending_fee_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_fee_requests.lock().unwrap().remove(&request_id) { let response = LSPS2FeeResponse { opening_fee_params_menu }; @@ -423,8 +423,8 @@ where cltv_expiry_delta, .. }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -437,7 +437,7 @@ where } if let Some(sender) = - lsps2_service.pending_buy_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_buy_requests.lock().unwrap().remove(&request_id) { let response = LSPS2BuyResponse { intercept_scid, cltv_expiry_delta }; @@ -477,7 +477,7 @@ where pub(crate) async fn lsps1_request_opening_params( &self, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); @@ -487,8 +487,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_opening_params_requests_lock = - lsps1_service.pending_opening_params_requests.lock().unwrap(); - let request_id = client_handler.request_supported_options(lsps1_service.node_id); + lsps1_client.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_client.lsp_node_id); pending_opening_params_requests_lock.insert(request_id, request_sender); } @@ -508,7 +508,7 @@ where &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, announce_channel: bool, refund_address: bitcoin::Address, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -559,16 +559,16 @@ where required_channel_confirmations: lsp_limits.min_required_channel_confirmations, funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, channel_expiry_blocks, - token: lsps1_service.token.clone(), + token: lsps1_client.token.clone(), announce_channel, }; let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_create_order_requests_lock = - lsps1_service.pending_create_order_requests.lock().unwrap(); + lsps1_client.pending_create_order_requests.lock().unwrap(); let request_id = client_handler.create_order( - &lsps1_service.node_id, + &lsps1_client.lsp_node_id, order_params.clone(), Some(refund_address), ); @@ -603,7 +603,7 @@ where pub(crate) async fn lsps1_check_order_status( &self, order_id: OrderId, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -612,8 +612,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_check_order_status_requests_lock = - lsps1_service.pending_check_order_status_requests.lock().unwrap(); - let request_id = client_handler.check_order_status(&lsps1_service.node_id, order_id); + lsps1_client.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_client.lsp_node_id, order_id); pending_check_order_status_requests_lock.insert(request_id, request_sender); } @@ -738,7 +738,7 @@ where } async fn lsps2_request_opening_fee_params(&self) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -747,9 +747,9 @@ where let (fee_request_sender, fee_request_receiver) = oneshot::channel(); { - let mut pending_fee_requests_lock = lsps2_service.pending_fee_requests.lock().unwrap(); + let mut pending_fee_requests_lock = lsps2_client.pending_fee_requests.lock().unwrap(); let request_id = client_handler - .request_opening_params(lsps2_service.node_id, lsps2_service.token.clone()); + .request_opening_params(lsps2_client.lsp_node_id, lsps2_client.token.clone()); pending_fee_requests_lock.insert(request_id, fee_request_sender); } @@ -771,7 +771,7 @@ where async fn lsps2_send_buy_request( &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -780,9 +780,9 @@ where let (buy_request_sender, buy_request_receiver) = oneshot::channel(); { - let mut pending_buy_requests_lock = lsps2_service.pending_buy_requests.lock().unwrap(); + let mut pending_buy_requests_lock = lsps2_client.pending_buy_requests.lock().unwrap(); let request_id = client_handler - .select_opening_params(lsps2_service.node_id, amount_msat, opening_fee_params) + .select_opening_params(lsps2_client.lsp_node_id, amount_msat, opening_fee_params) .map_err(|e| { log_error!( self.logger, @@ -815,7 +815,7 @@ where &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &str, expiry_secs: u32, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // 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; @@ -828,7 +828,7 @@ where })?; let route_hint = RouteHint(vec![RouteHintHop { - src_node_id: lsps2_service.node_id, + src_node_id: lsps2_client.lsp_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, @@ -932,9 +932,8 @@ impl Lsps1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); @@ -978,9 +977,8 @@ impl Lsps1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index cd6540274..8ed56de89 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -575,9 +575,8 @@ impl Bolt11Payment { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (node_id, address) = liquidity_source - .get_lsps2_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); From 32095c8e45074ddda43a918541fcbacf962c6891 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:01:52 +0100 Subject: [PATCH 09/13] Refactor `derive_xprv` to make it reusble .. and while we're at it we move the VSS child key indexes to constants. --- src/builder.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 8bfa57ade..4629ce608 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -72,6 +72,9 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::SystemTime; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; +const VSS_HARDENED_CHILD_INDEX: u32 = 877; +const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; + #[derive(Debug, Clone)] enum ChainDataSourceConfig { Esplora { server_url: String, sync_config: Option }, @@ -429,10 +432,14 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = + derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?; let lnurl_auth_xprv = vss_xprv - .derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 138 }]) + .derive_priv( + &Secp256k1::new(), + &[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }], + ) .map_err(|e| { log_error!(logger, "Failed to derive VSS secret: {}", e); BuildError::KVStoreSetupFailed @@ -494,7 +501,12 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = derive_xprv( + config.clone(), + &seed_bytes, + VSS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes(); @@ -1316,8 +1328,9 @@ fn seed_bytes_from_config( } } -fn derive_vss_xprv( - config: Arc, seed_bytes: &[u8; 64], logger: Arc, +fn derive_xprv( + config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, + logger: Arc, ) -> Result { use bitcoin::key::Secp256k1; @@ -1326,10 +1339,11 @@ fn derive_vss_xprv( BuildError::InvalidSeedBytes })?; - xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| { - log_error!(logger, "Failed to derive VSS secret: {}", e); - BuildError::KVStoreSetupFailed - }) + xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }]) + .map_err(|e| { + log_error!(logger, "Failed to derive hardened child secret: {}", e); + BuildError::InvalidSeedBytes + }) } /// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string. From 9d65dc9459d6ee1a9fc945b9387ede1144aacb4b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:17:47 +0100 Subject: [PATCH 10/13] Allow configuring LSPS2 service via builders We add the capability to configure LSPS2 service mode in `Builder` and `LiquiditySourceBuilder`. --- src/builder.rs | 118 +++++++++++++++++++++++++++++++++++------------ src/liquidity.rs | 30 +++++++++++- 2 files changed, 117 insertions(+), 31 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 4629ce608..1b15f9b51 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -74,6 +74,7 @@ use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvide const VSS_HARDENED_CHILD_INDEX: u32 = 877; const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; +const LSPS_HARDENED_CHILD_INDEX: u32 = 577; #[derive(Debug, Clone)] enum ChainDataSourceConfig { @@ -100,6 +101,8 @@ struct LiquiditySourceConfig { lsps1_client: Option, // Act as an LSPS2 client connecting to the given service. lsps2_client: Option, + // Act as an LSPS2 service. + lsps2_service: Option, } #[derive(Debug, Clone)] @@ -116,6 +119,12 @@ struct LSPS2ClientConfig { token: Option, } +#[derive(Debug, Clone)] +struct LSPS2ServiceConfig { + token: Option, + advertise_service: bool, +} + /// An error encountered during building a [`Node`]. /// /// [`Node`]: crate::Node @@ -323,6 +332,26 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// If a `token` is provided, only requests matching this token will be accepted. + /// + /// If `advertise_service` is set, the LSPS service will be announced via the gossip network. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2( + &mut self, token: Option, advertise_service: bool, + ) -> &mut Self { + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + let lsps2_service_config = LSPS2ServiceConfig { token, advertise_service }; + liquidity_source_config.lsps2_service = Some(lsps2_service_config); + self + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&mut self, storage_dir_path: String) -> &mut Self { self.config.storage_dir_path = storage_dir_path; @@ -658,6 +687,20 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// If a `token` is provided, only requests matching this token will be accepted. + /// + /// If `advertise_service` is set, the LSPS service will be announced via the gossip network. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2(&self, token: Option, advertise_service: bool) { + self.inner.write().unwrap().set_liquidity_provider_lsps2(token, advertise_service); + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().unwrap().set_storage_dir_path(storage_dir_path); @@ -1109,39 +1152,56 @@ fn build_with_store_internal( }, }; - let liquidity_source = liquidity_source_config.as_ref().map(|lsc| { - let mut liquidity_source_builder = LiquiditySourceBuilder::new( - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - Arc::clone(&chain_source), - Arc::clone(&config), - Arc::clone(&logger), - ); + let (liquidity_source, custom_message_handler) = + if let Some(lsc) = liquidity_source_config.as_ref() { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&chain_source), + Arc::clone(&config), + Arc::clone(&logger), + ); - lsc.lsps1_client.as_ref().map(|config| { - liquidity_source_builder.lsps1_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - lsc.lsps2_client.as_ref().map(|config| { - liquidity_source_builder.lsps2_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - Arc::new(liquidity_source_builder.build()) - }); + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() + }; + lsc.lsps2_service.as_ref().map(|config| { + liquidity_source_builder.lsps2_service( + promise_secret, + config.token.clone(), + config.advertise_service, + ) + }); - let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))) - } else { - Arc::new(NodeCustomMessageHandler::new_ignoring()) - }; + let liquidity_source = Arc::new(liquidity_source_builder.build()); + let custom_message_handler = + Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); + (Some(liquidity_source), custom_message_handler) + } else { + (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + }; let msg_handler = match gossip_source.as_gossip_sync() { GossipSync::P2P(p2p_gossip_sync) => MessageHandler { diff --git a/src/liquidity.rs b/src/liquidity.rs index cf38d711b..daa049693 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -27,8 +27,9 @@ use lightning_liquidity::lsps1::msgs::{ use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; -use lightning_liquidity::LiquidityClientConfig; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -63,12 +64,19 @@ struct LSPS2Client { pending_buy_requests: Mutex>>, } +struct LSPS2Service { + token: Option, + service_config: LSPS2ServiceConfig, + advertise_service: bool, +} + pub(crate) struct LiquiditySourceBuilder where L::Target: Logger, { lsps1_client: Option, lsps2_client: Option, + lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -86,9 +94,11 @@ where ) -> Self { let lsps1_client = None; let lsps2_client = None; + let lsps2_service = None; Self { lsps1_client, lsps2_client, + lsps2_service, channel_manager, keys_manager, chain_source, @@ -134,7 +144,21 @@ where self } + pub(crate) fn lsps2_service( + &mut self, promise_secret: [u8; 32], token: Option, advertise_service: bool, + ) -> &mut Self { + let service_config = LSPS2ServiceConfig { promise_secret }; + self.lsps2_service = Some(LSPS2Service { token, service_config, advertise_service }); + self + } + pub(crate) fn build(self) -> LiquiditySource { + let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { + let lsps2_service_config = Some(s.service_config.clone()); + let advertise_service = s.advertise_service; + LiquidityServiceConfig { lsps2_service_config, advertise_service } + }); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.client_config.clone()); let liquidity_client_config = @@ -145,13 +169,14 @@ where Arc::clone(&self.channel_manager), Some(Arc::clone(&self.chain_source)), None, - None, + liquidity_service_config, liquidity_client_config, )); LiquiditySource { lsps1_client: self.lsps1_client, lsps2_client: self.lsps2_client, + lsps2_service: self.lsps2_service, channel_manager: self.channel_manager, keys_manager: self.keys_manager, liquidity_manager, @@ -167,6 +192,7 @@ where { lsps1_client: Option, lsps2_client: Option, + lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, From af6bd7f1d90657a25abc7e564902f05677624e2f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:44:02 +0100 Subject: [PATCH 11/13] Add LDK event handling .. and forward it to our `LiquditySource`. --- src/event.rs | 41 ++++++++++++++++++++++++++++++++++--- src/lib.rs | 1 + src/liquidity.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/event.rs b/src/event.rs index 1de77e937..3e0307518 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,6 +14,7 @@ use crate::{ use crate::connection::ConnectionManager; use crate::fee_estimator::ConfirmationTarget; +use crate::liquidity::LiquiditySource; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, @@ -24,7 +25,7 @@ use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; -use crate::logger::{log_debug, log_error, log_info, Logger}; +use crate::logger::{log_debug, log_error, log_info, FilesystemLogger, Logger}; use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ClosureReason, PaymentPurpose, ReplayEvent}; @@ -445,6 +446,7 @@ where connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, @@ -461,6 +463,7 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, logger: L, config: Arc, ) -> Self { @@ -472,6 +475,7 @@ where connection_manager, output_sweeper, network_graph, + liquidity_source, payment_store, peer_store, logger, @@ -1009,7 +1013,11 @@ where LdkEvent::PaymentPathFailed { .. } => {}, LdkEvent::ProbeSuccessful { .. } => {}, LdkEvent::ProbeFailed { .. } => {}, - LdkEvent::HTLCHandlingFailed { .. } => {}, + LdkEvent::HTLCHandlingFailed { failed_next_destination, .. } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_handling_failed(failed_next_destination); + } + }, LdkEvent::PendingHTLCsForwardable { time_forwardable } => { let forwarding_channel_manager = self.channel_manager.clone(); let min = time_forwardable.as_millis() as u64; @@ -1230,6 +1238,10 @@ where fee_earned, ); } + + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_payment_forwarded(next_channel_id); + } }, LdkEvent::ChannelPending { channel_id, @@ -1303,6 +1315,14 @@ where counterparty_node_id, ); + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_channel_ready( + user_channel_id, + &channel_id, + &counterparty_node_id, + ); + } + let event = Event::ChannelReady { channel_id, user_channel_id: UserChannelId(user_channel_id), @@ -1341,7 +1361,22 @@ where }; }, LdkEvent::DiscardFunding { .. } => {}, - LdkEvent::HTLCIntercepted { .. } => {}, + LdkEvent::HTLCIntercepted { + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + .. + } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_intercepted( + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ); + } + }, LdkEvent::InvoiceReceived { .. } => { debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, diff --git a/src/lib.rs b/src/lib.rs index 01e6fdb32..088b7032d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -535,6 +535,7 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), + self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), diff --git a/src/liquidity.rs b/src/liquidity.rs index daa049693..75cb18a31 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -13,8 +13,11 @@ use crate::logger::{log_debug, log_error, log_info, FilesystemLogger, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{Config, Error}; -use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use lightning::events::HTLCDestination; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; +use lightning::ln::types::ChannelId; +use lightning::ln::PaymentHash; use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; @@ -891,6 +894,54 @@ where Error::InvoiceCreationFailed }) } + + pub(crate) fn handle_channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.channel_ready( + user_channel_id, + channel_id, + counterparty_node_id, + ) { + log_error!(self.logger, "Errored processing ChannelReady event: {:?}", e); + } + } + } + + pub(crate) fn handle_htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) { + log_error!(self.logger, "Failed to handle HTLCIntercepted event: {:?}", e); + } + } + } + + pub(crate) fn handle_htlc_handling_failed(&self, failed_next_destination: HTLCDestination) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failed_next_destination) { + log_error!(self.logger, "Errored processing HTLCHandlingFailed event: {:?}", e); + } + } + } + + pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option) { + if let Some(next_channel_id) = next_channel_id { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id) { + log_error!(self.logger, "Failed to handle PaymentForwarded: {:?}", e); + } + } + } + } } #[derive(Debug, Clone)] From c61a12b27116ec58024bf50eb6c9647651882924 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:53:43 +0100 Subject: [PATCH 12/13] Move `PaymentForwarded` event emission down .. to align with other event handling variants: First log, then act, then emit event if everything went okay. --- src/event.rs | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/event.rs b/src/event.rs index 3e0307518..c2ee58ea9 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1169,23 +1169,6 @@ where claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { - let event = Event::PaymentForwarded { - prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), - next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), - prev_user_channel_id: prev_user_channel_id.map(UserChannelId), - next_user_channel_id: next_user_channel_id.map(UserChannelId), - prev_node_id, - next_node_id, - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx, - outbound_amount_forwarded_msat, - }; - self.event_queue.add_event(event).map_err(|e| { - log_error!(self.logger, "Failed to push to event queue: {}", e); - ReplayEvent() - })?; - let read_only_network_graph = self.network_graph.read_only(); let nodes = read_only_network_graph.nodes(); let channels = self.channel_manager.list_channels(); @@ -1218,14 +1201,13 @@ where format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); let fee_earned = total_fee_earned_msat.unwrap_or(0); - let outbound_amount_forwarded_msat = outbound_amount_forwarded_msat.unwrap_or(0); if claim_from_onchain_tx { log_info!( self.logger, "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } else { @@ -1234,7 +1216,7 @@ where "Forwarded payment{}{} of {}msat, earning {}msat in fees.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } @@ -1242,6 +1224,23 @@ where if let Some(liquidity_source) = self.liquidity_source.as_ref() { liquidity_source.handle_payment_forwarded(next_channel_id); } + + let event = Event::PaymentForwarded { + prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), + next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), + prev_user_channel_id: prev_user_channel_id.map(UserChannelId), + next_user_channel_id: next_user_channel_id.map(UserChannelId), + prev_node_id, + next_node_id, + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx, + outbound_amount_forwarded_msat, + }; + self.event_queue.add_event(event).map_err(|e| { + log_error!(self.logger, "Failed to push to event queue: {}", e); + ReplayEvent() + })?; }, LdkEvent::ChannelPending { channel_id, From dfdef1d3022d64b99c3b90080ae6c251ade9c6d4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 14:27:59 +0100 Subject: [PATCH 13/13] WIP Add `LSPS2ServiceEvent` handling .. so far with dummy values, we'll have to discuss how the API should look like. --- src/liquidity.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 75cb18a31..b52dda934 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -28,8 +28,8 @@ use lightning_liquidity::lsps1::msgs::{ ChannelInfo, LSPS1Options, OrderId, OrderParameters, PaymentInfo, }; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; -use lightning_liquidity::lsps2::event::LSPS2ClientEvent; -use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -39,6 +39,8 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; +use chrono::{DateTime, Utc}; + use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex, RwLock}; @@ -395,6 +397,111 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, + Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token: _, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let min_fee_msat = 0; + let proportional = 0; + let mut valid_until: DateTime = Utc::now(); + valid_until += chrono::Duration::minutes(10); + let min_lifetime = 1008; + let max_client_to_self_delay = 144; + let min_payment_size_msat = 1000; + let max_payment_size_msat = 10_000_000_000; + + let opening_fee_params = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until, + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + max_payment_size_msat, + }; + + let opening_fee_params_menu = vec![opening_fee_params]; + + if let Err(e) = lsps2_service_handler.opening_fee_params_generated( + &counterparty_node_id, + request_id, + opening_fee_params_menu, + ) { + log_error!( + self.logger, + "Failed to handle generated opening fee params: {:?}", + e + ); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: _, + payment_size_msat: _, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let user_channel_id = 0; + let scid = self.channel_manager.get_intercept_scid(); + let cltv_expiry_delta = 72; + let client_trusts_lsp = true; + + if let Err(e) = lsps2_service_handler.invoice_parameters_generated( + &counterparty_node_id, + request_id, + scid, + cltv_expiry_delta, + client_trusts_lsp, + user_channel_id, + ) { + log_error!(self.logger, "Failed to provide invoice parameters: {:?}", e); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat: _, + user_channel_id, + intercept_scid: _, + }) => { + if self.liquidity_manager.lsps2_service_handler().is_none() { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let channel_size_sats = (amt_to_forward_msat / 1000) * 4; + let mut config = *self.channel_manager.get_current_default_configuration(); + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_config.forwarding_fee_base_msat = 0; + config.channel_config.forwarding_fee_proportional_millionths = 0; + + if let Err(e) = self.channel_manager.create_channel( + their_network_key, + channel_size_sats, + 0, + user_channel_id, + None, + Some(config), + ) { + log_error!(self.logger, "Failed to open jit channel: {:?}", e); + } + }, Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id,