From 46cee54cb8a2a6699e7ad320d87cde3e9b866077 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Mon, 16 Jun 2025 11:58:23 -0300 Subject: [PATCH 01/27] LSPS1: Add initial integration test We add the first LSPS1 integration test. This is based on the unfinished work in https://github.com/lightningdevkit/rust-lightning/pull/3864, but rebased to account for the new ways we now do integration test setup. --- .../tests/lsps1_integration_tests.rs | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 lightning-liquidity/tests/lsps1_integration_tests.rs diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs new file mode 100644 index 00000000000..5e842c6a111 --- /dev/null +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -0,0 +1,273 @@ +#![cfg(all(test, feature = "time", lsps1_service))] + +mod common; + +use common::create_service_and_client_nodes_with_kv_stores; +use common::{get_lsps_message, LSPSNodes}; + +use lightning::ln::peer_handler::CustomMessageHandler; +use lightning_liquidity::events::LiquidityEvent; +use lightning_liquidity::lsps0::ser::LSPSDateTime; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps1::event::LSPS1ClientEvent; +use lightning_liquidity::lsps1::event::LSPS1ServiceEvent; +use lightning_liquidity::lsps1::msgs::LSPS1OrderState; +use lightning_liquidity::lsps1::msgs::{ + LSPS1OnchainPaymentInfo, LSPS1Options, LSPS1OrderParams, LSPS1PaymentInfo, +}; +use lightning_liquidity::lsps1::service::LSPS1ServiceConfig; +use lightning_liquidity::utils::time::DefaultTimeProvider; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; + +use lightning::ln::functional_test_utils::{ + create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs, +}; +use lightning::util::test_utils::TestStore; + +use std::str::FromStr; +use std::sync::Arc; + +use lightning::ln::functional_test_utils::{create_network, Node}; + +fn build_lsps1_configs( + supported_options: LSPS1Options, +) -> (LiquidityServiceConfig, LiquidityClientConfig) { + let lsps1_service_config = + LSPS1ServiceConfig { token: None, supported_options: Some(supported_options) }; + let service_config = LiquidityServiceConfig { + lsps1_service_config: Some(lsps1_service_config), + lsps2_service_config: None, + lsps5_service_config: None, + advertise_service: true, + }; + + let lsps1_client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let client_config = LiquidityClientConfig { + lsps1_client_config: Some(lsps1_client_config), + lsps2_client_config: None, + lsps5_client_config: None, + }; + + (service_config, client_config) +} + +fn setup_test_lsps1_nodes_with_kv_stores<'a, 'b, 'c>( + nodes: Vec>, service_kv_store: Arc, + client_kv_store: Arc, supported_options: LSPS1Options, +) -> LSPSNodes<'a, 'b, 'c> { + let (service_config, client_config) = build_lsps1_configs(supported_options); + let lsps_nodes = create_service_and_client_nodes_with_kv_stores( + nodes, + service_config, + client_config, + Arc::new(DefaultTimeProvider), + service_kv_store, + client_kv_store, + ); + lsps_nodes +} + +fn setup_test_lsps1_nodes<'a, 'b, 'c>( + nodes: Vec>, supported_options: LSPS1Options, +) -> LSPSNodes<'a, 'b, 'c> { + let service_kv_store = Arc::new(TestStore::new(false)); + let client_kv_store = Arc::new(TestStore::new(false)); + setup_test_lsps1_nodes_with_kv_stores( + nodes, + service_kv_store, + client_kv_store, + supported_options, + ) +} + +#[test] +fn lsps1_happy_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let expected_options_supported = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, expected_options_supported.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + let request_supported_options_id = client_handler.request_supported_options(service_node_id); + let request_supported_options = get_lsps_message!(client_node, service_node_id); + + service_node + .liquidity_manager + .handle_custom_message(request_supported_options, client_node_id) + .unwrap(); + + let get_info_message = get_lsps_message!(service_node, client_node_id); + + client_node.liquidity_manager.handle_custom_message(get_info_message, service_node_id).unwrap(); + + let get_info_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + request_id, + counterparty_node_id, + supported_options, + }) = get_info_event + { + assert_eq!(request_id, request_supported_options_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(expected_options_supported, supported_options); + } else { + panic!("Unexpected event"); + } + + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let _create_order_id = + client_handler.create_order(&service_node_id, order_params.clone(), None); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let _request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id, + order, + }) = _request_for_payment_event + { + assert_eq!(request_id, _create_order_id.clone()); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(order, order_params); + } else { + panic!("Unexpected event"); + } + + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2025-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + let _now = LSPSDateTime::from_str("2024-01-01T00:00:00Z").expect("Failed to parse date"); + + let _ = service_handler + .send_payment_details(_create_order_id.clone(), &client_node_id, payment_info.clone(), _now) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + let expected_order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) = order_created_event + { + assert_eq!(request_id, _create_order_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(order, order_params); + assert_eq!(payment, payment_info); + assert!(channel.is_none()); + order_id + } else { + panic!("Unexpected event"); + }; + + let check_order_status_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + let check_order_status = get_lsps_message!(client_node, service_node_id); + + service_node + .liquidity_manager + .handle_custom_message(check_order_status, client_node_id) + .unwrap(); + + let _check_payment_confirmation_event = service_node.liquidity_manager.next_event().unwrap(); + + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::CheckPaymentConfirmation { + request_id, + counterparty_node_id, + order_id, + }) = _check_payment_confirmation_event + { + assert_eq!(request_id, check_order_status_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(order_id, expected_order_id.clone()); + } else { + panic!("Unexpected event"); + } + + let _ = service_handler + .update_order_status( + check_order_status_id.clone(), + client_node_id, + expected_order_id.clone(), + LSPS1OrderState::Created, + None, + ) + .unwrap(); + + let order_status_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(order_status_response, service_node_id) + .unwrap(); + + let order_status_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) = order_status_event + { + assert_eq!(request_id, check_order_status_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(order, order_params); + assert_eq!(payment, payment_info); + assert!(channel.is_none()); + assert_eq!(order_id, expected_order_id); + } else { + panic!("Unexpected event"); + } +} From 4290b9343a9bb481257316919072222145a1824f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 12:28:40 +0100 Subject: [PATCH 02/27] Cleanup unused code .. for which we got warnings --- lightning-liquidity/src/lsps1/service.rs | 26 ++++-------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 8afea1b4345..00faca983d5 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -40,8 +40,6 @@ use lightning::util::persist::KVStore; use bitcoin::secp256k1::PublicKey; -use chrono::Utc; - /// Server-side configuration options for bLIP-51 / LSPS1 channel requests. #[derive(Clone, Debug)] pub struct LSPS1ServiceConfig { @@ -63,7 +61,6 @@ impl From for LightningError { enum OutboundRequestState { OrderCreated { order_id: LSPS1OrderId }, WaitingPayment { order_id: LSPS1OrderId }, - Ready, } impl OutboundRequestState { @@ -102,18 +99,11 @@ impl OutboundCRChannel { self.state = self.state.awaiting_payment()?; Ok(()) } - - fn check_order_validity(&self, supported_options: &LSPS1Options) -> bool { - let order = &self.config.order; - - is_valid(order, supported_options) - } } #[derive(Default)] struct PeerState { outbound_channels_by_order_id: HashMap, - request_to_cid: HashMap, pending_requests: HashMap, } @@ -121,14 +111,6 @@ impl PeerState { fn insert_outbound_channel(&mut self, order_id: LSPS1OrderId, channel: OutboundCRChannel) { self.outbound_channels_by_order_id.insert(order_id, channel); } - - fn insert_request(&mut self, request_id: LSPSRequestId, channel_id: u128) { - self.request_to_cid.insert(request_id, channel_id); - } - - fn remove_outbound_channel(&mut self, order_id: LSPS1OrderId) { - self.outbound_channels_by_order_id.remove(&order_id); - } } /// The main object allowing to send and receive bLIP-51 / LSPS1 messages. @@ -140,8 +122,8 @@ where K::Target: KVStore, { entropy_source: ES, - channel_manager: CM, - chain_source: Option, + _channel_manager: CM, + _chain_source: Option, pending_messages: Arc, pending_events: Arc>, per_peer_state: RwLock>>, @@ -164,8 +146,8 @@ where ) -> Self { Self { entropy_source, - channel_manager, - chain_source, + _channel_manager: channel_manager, + _chain_source: chain_source, pending_messages, pending_events, per_peer_state: RwLock::new(new_hash_map()), From 838b6e801247bc7215e08351658f64bbfd61aaca Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 9 Dec 2025 12:08:55 +0100 Subject: [PATCH 03/27] Drop `chain_source` from `LSPS1ServiceHandler` We previously considered tracking payment confirmations as part of the handler. However, we can considerably simplify our logic if we stick with the current approach of having the LSPs track the payment status and update us when prompted through events. --- lightning-liquidity/src/lsps1/service.rs | 17 +++++------------ lightning-liquidity/src/manager.rs | 9 ++++----- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 00faca983d5..ff37d2506e9 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -30,7 +30,6 @@ use crate::prelude::{new_hash_map, HashMap}; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils; -use lightning::chain::Filter; use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::sign::EntropySource; @@ -114,40 +113,35 @@ impl PeerState { } /// The main object allowing to send and receive bLIP-51 / LSPS1 messages. -pub struct LSPS1ServiceHandler +pub struct LSPS1ServiceHandler where ES::Target: EntropySource, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, { entropy_source: ES, _channel_manager: CM, - _chain_source: Option, pending_messages: Arc, pending_events: Arc>, per_peer_state: RwLock>>, config: LSPS1ServiceConfig, } -impl LSPS1ServiceHandler +impl LSPS1ServiceHandler where ES::Target: EntropySource, CM::Target: AChannelManager, - C::Target: Filter, ES::Target: EntropySource, K::Target: KVStore, { /// Constructs a `LSPS1ServiceHandler`. pub(crate) fn new( entropy_source: ES, pending_messages: Arc, - pending_events: Arc>, channel_manager: CM, chain_source: Option, - config: LSPS1ServiceConfig, + pending_events: Arc>, channel_manager: CM, config: LSPS1ServiceConfig, ) -> Self { Self { entropy_source, _channel_manager: channel_manager, - _chain_source: chain_source, pending_messages, pending_events, per_peer_state: RwLock::new(new_hash_map()), @@ -403,12 +397,11 @@ where } } -impl LSPSProtocolMessageHandler - for LSPS1ServiceHandler +impl LSPSProtocolMessageHandler + for LSPS1ServiceHandler where ES::Target: EntropySource, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, { type ProtocolMessage = LSPS1Message; diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 0b4f5efaa3c..14ce0c8bd2b 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -322,7 +322,7 @@ pub struct LiquidityManager< lsps0_client_handler: LSPS0ClientHandler, lsps0_service_handler: Option, #[cfg(lsps1_service)] - lsps1_service_handler: Option>, + lsps1_service_handler: Option>, lsps1_client_handler: Option>, lsps2_service_handler: Option>, lsps2_client_handler: Option>, @@ -509,7 +509,7 @@ where #[cfg(lsps1_service)] let lsps1_service_handler = service_config.as_ref().and_then(|config| { if let Some(number) = - as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER + as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER { supported_protocols.push(number); } @@ -519,7 +519,6 @@ where Arc::clone(&pending_messages), Arc::clone(&pending_events), channel_manager.clone(), - chain_source.clone(), config.clone(), ) }) @@ -579,7 +578,7 @@ where /// Returns a reference to the LSPS1 server-side handler. #[cfg(lsps1_service)] - pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler> { + pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler> { self.lsps1_service_handler.as_ref() } @@ -1215,7 +1214,7 @@ where #[cfg(lsps1_service)] pub fn lsps1_service_handler( &self, - ) -> Option<&LSPS1ServiceHandler>> { + ) -> Option<&LSPS1ServiceHandler>> { self.inner.lsps1_service_handler() } From 195505d56b837133e2f1748427477bf9076a6e63 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 9 Dec 2025 12:24:41 +0100 Subject: [PATCH 04/27] Drop `Listen`/`Confirm`/etc from `LiquidityManager` Now that we don't do on-chain tracking in LSPS1, we can drop quite a few `LiquidityManager` parameters and generics, which were only added in anticipation of tracking on-chain state. --- fuzz/src/lsps_message.rs | 2 - lightning-background-processor/src/lib.rs | 29 +- lightning-liquidity/src/manager.rs | 260 ++---------------- lightning-liquidity/tests/common/mod.rs | 15 - .../tests/lsps2_integration_tests.rs | 10 +- .../tests/lsps5_integration_tests.rs | 13 +- 6 files changed, 37 insertions(+), 292 deletions(-) diff --git a/fuzz/src/lsps_message.rs b/fuzz/src/lsps_message.rs index 547a27b70ee..770ca99eca7 100644 --- a/fuzz/src/lsps_message.rs +++ b/fuzz/src/lsps_message.rs @@ -81,8 +81,6 @@ pub fn do_test(data: &[u8]) { Arc::clone(&keys_manager), Arc::clone(&keys_manager), Arc::clone(&manager), - None::>, - None, kv_store, Arc::clone(&tx_broadcaster), None, diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index aae738ab1c1..675e63eb0ae 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -467,8 +467,6 @@ pub const NO_LIQUIDITY_MANAGER: Option< NS = &(dyn lightning::sign::NodeSigner + Send + Sync), AChannelManager = DynChannelManager, CM = &DynChannelManager, - Filter = dyn chain::Filter + Send + Sync, - C = &(dyn chain::Filter + Send + Sync), KVStore = DummyKVStore, K = &DummyKVStore, TimeProvider = dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync, @@ -494,8 +492,6 @@ pub const NO_LIQUIDITY_MANAGER_SYNC: Option< NS = &(dyn lightning::sign::NodeSigner + Send + Sync), AChannelManager = DynChannelManager, CM = &DynChannelManager, - Filter = dyn chain::Filter + Send + Sync, - C = &(dyn chain::Filter + Send + Sync), KVStoreSync = dyn lightning::util::persist::KVStoreSync + Send + Sync, KS = &(dyn lightning::util::persist::KVStoreSync + Send + Sync), TimeProvider = dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync, @@ -823,7 +819,7 @@ use futures_util::{dummy_waker, Joiner, OptionalSelector, Selector, SelectorOutp /// # type P2PGossipSync
    = lightning::routing::gossip::P2PGossipSync, Arc
      , Arc>; /// # type ChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager, B, FE, Logger>; /// # type OnionMessenger = lightning::onion_message::messenger::OnionMessenger, Arc, Arc, Arc>, Arc, Arc, Arc>>, Arc>, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler>; -/// # type LiquidityManager = lightning_liquidity::LiquidityManager, Arc, Arc>, Arc, Arc, Arc, Arc>; +/// # type LiquidityManager = lightning_liquidity::LiquidityManager, Arc, Arc>, Arc, Arc, Arc>; /// # type Scorer = RwLock, Arc>>; /// # type PeerManager = lightning::ln::peer_handler::SimpleArcPeerManager, B, FE, Arc
        , Logger, F, StoreSync>; /// # type OutputSweeper = lightning::util::sweep::OutputSweeper, Arc, Arc, Arc, Arc, Arc, Arc>; @@ -1872,7 +1868,7 @@ mod tests { use core::sync::atomic::{AtomicBool, Ordering}; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::chain::transaction::OutPoint; - use lightning::chain::{chainmonitor, BestBlock, Confirm, Filter}; + use lightning::chain::{chainmonitor, BestBlock, Confirm}; use lightning::events::{Event, PathFailure, ReplayEvent}; use lightning::ln::channelmanager; use lightning::ln::channelmanager::{ @@ -2008,7 +2004,6 @@ mod tests { Arc, Arc, Arc, - Arc, Arc, DefaultTimeProvider, Arc, @@ -2465,8 +2460,6 @@ mod tests { Arc::clone(&keys_manager), Arc::clone(&keys_manager), Arc::clone(&manager), - None, - None, Arc::clone(&kv_store), Arc::clone(&tx_broadcaster), None, @@ -2843,10 +2836,10 @@ mod tests { let kv_store = KVStoreSyncWrapper(kv_store_sync); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) @@ -3362,10 +3355,10 @@ mod tests { let kv_store = KVStoreSyncWrapper(kv_store_sync); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) @@ -3589,10 +3582,10 @@ mod tests { let (exit_sender, exit_receiver) = tokio::sync::watch::channel(()); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 14ce0c8bd2b..1116f7d27e4 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -43,8 +43,7 @@ use crate::utils::time::DefaultTimeProvider; use crate::utils::time::TimeProvider; use lightning::chain::chaininterface::BroadcasterInterface; -use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; -use lightning::ln::channelmanager::{AChannelManager, ChainParameters}; +use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; @@ -115,10 +114,6 @@ pub trait ALiquidityManager { type AChannelManager: AChannelManager + ?Sized; /// A type that may be dereferenced to [`Self::AChannelManager`]. type CM: Deref + Clone; - /// A type implementing [`Filter`]. - type Filter: Filter + ?Sized; - /// A type that may be dereferenced to [`Self::Filter`]. - type C: Deref + Clone; /// A type implementing [`KVStore`]. type KVStore: KVStore + ?Sized; /// A type that may be dereferenced to [`Self::KVStore`]. @@ -132,25 +127,22 @@ pub trait ALiquidityManager { /// A type that may be dereferenced to [`Self::BroadcasterInterface`]. type T: Deref + Clone; /// Returns a reference to the actual [`LiquidityManager`] object. - fn get_lm( - &self, - ) -> &LiquidityManager; + fn get_lm(&self) + -> &LiquidityManager; } impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > ALiquidityManager for LiquidityManager + > ALiquidityManager for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -161,15 +153,13 @@ where type NS = NS; type AChannelManager = CM::Target; type CM = CM; - type Filter = C::Target; - type C = C; type KVStore = K::Target; type K = K; type TimeProvider = TP::Target; type TP = TP; type BroadcasterInterface = T::Target; type T = T; - fn get_lm(&self) -> &LiquidityManager { + fn get_lm(&self) -> &LiquidityManager { self } } @@ -191,10 +181,6 @@ pub trait ALiquidityManagerSync { type AChannelManager: AChannelManager + ?Sized; /// A type that may be dereferenced to [`Self::AChannelManager`]. type CM: Deref + Clone; - /// A type implementing [`Filter`]. - type Filter: Filter + ?Sized; - /// A type that may be dereferenced to [`Self::Filter`]. - type C: Deref + Clone; /// A type implementing [`KVStoreSync`]. type KVStoreSync: KVStoreSync + ?Sized; /// A type that may be dereferenced to [`Self::KVStoreSync`]. @@ -215,7 +201,6 @@ pub trait ALiquidityManagerSync { Self::ES, Self::NS, Self::CM, - Self::C, KVStoreSyncWrapper, Self::TP, Self::T, @@ -223,23 +208,21 @@ pub trait ALiquidityManagerSync { /// Returns a reference to the actual [`LiquidityManager`] object. fn get_lm( &self, - ) -> &LiquidityManagerSync; + ) -> &LiquidityManagerSync; } impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > ALiquidityManagerSync for LiquidityManagerSync + > ALiquidityManagerSync for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -250,8 +233,6 @@ where type NS = NS; type AChannelManager = CM::Target; type CM = CM; - type Filter = C::Target; - type C = C; type KVStoreSync = KS::Target; type KS = KS; type TimeProvider = TP::Target; @@ -266,14 +247,13 @@ where Self::ES, Self::NS, Self::CM, - Self::C, KVStoreSyncWrapper, Self::TP, Self::T, > { &self.inner } - fn get_lm(&self) -> &LiquidityManagerSync { + fn get_lm(&self) -> &LiquidityManagerSync { self } } @@ -301,7 +281,6 @@ pub struct LiquidityManager< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, @@ -309,7 +288,6 @@ pub struct LiquidityManager< ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -330,8 +308,6 @@ pub struct LiquidityManager< lsps5_client_handler: Option>, service_config: Option, _client_config: Option, - best_block: RwLock>, - _chain_source: Option, pending_msgs_or_needs_persist_notifier: Arc, } @@ -340,15 +316,13 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, K: Deref + Clone, T: Deref + Clone, - > LiquidityManager + > LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, T::Target: BroadcasterInterface, { @@ -356,9 +330,8 @@ where /// /// Will read persisted service states from the given [`KVStore`]. pub async fn new( - entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option, - chain_params: Option, kv_store: K, transaction_broadcaster: T, - service_config: Option, + entropy_source: ES, node_signer: NS, channel_manager: CM, kv_store: K, + transaction_broadcaster: T, service_config: Option, client_config: Option, ) -> Result { Self::new_with_custom_time_provider( @@ -366,8 +339,6 @@ where node_signer, channel_manager, transaction_broadcaster, - chain_source, - chain_params, kv_store, service_config, client_config, @@ -381,16 +352,14 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > LiquidityManager + > LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -405,8 +374,7 @@ where /// [`LiquidityClientConfig`] and [`LiquidityServiceConfig`]. pub async fn new_with_custom_time_provider( entropy_source: ES, node_signer: NS, channel_manager: CM, transaction_broadcaster: T, - chain_source: Option, chain_params: Option, kv_store: K, - service_config: Option, + kv_store: K, service_config: Option, client_config: Option, time_provider: TP, ) -> Result { let pending_msgs_or_needs_persist_notifier = Arc::new(Notifier::new()); @@ -552,8 +520,6 @@ where lsps5_service_handler, service_config, _client_config: client_config, - best_block: RwLock::new(chain_params.map(|chain_params| chain_params.best_block)), - _chain_source: chain_source, pending_msgs_or_needs_persist_notifier, }) } @@ -807,16 +773,14 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageReader for LiquidityManager + > CustomMessageReader for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -839,16 +803,14 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageHandler for LiquidityManager + > CustomMessageHandler for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, K::Target: KVStore, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -969,103 +931,12 @@ where } } -impl< - ES: Deref + Clone, - NS: Deref + Clone, - CM: Deref + Clone, - C: Deref + Clone, - K: Deref + Clone, - TP: Deref + Clone, - T: Deref + Clone, - > Listen for LiquidityManager -where - ES::Target: EntropySource, - NS::Target: NodeSigner, - CM::Target: AChannelManager, - C::Target: Filter, - K::Target: KVStore, - TP::Target: TimeProvider, - T::Target: BroadcasterInterface, -{ - fn filtered_block_connected( - &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - if let Some(best_block) = self.best_block.read().unwrap().as_ref() { - assert_eq!(best_block.block_hash, header.prev_blockhash, - "Blocks must be connected in chain-order - the connected header must build on the last connected header"); - assert_eq!(best_block.height, height - 1, - "Blocks must be connected in chain-order - the connected block height must be one greater than the previous height"); - } - - self.transactions_confirmed(header, txdata, height); - self.best_block_updated(header, height); - } - - fn blocks_disconnected(&self, fork_point: BestBlock) { - if let Some(best_block) = self.best_block.write().unwrap().as_mut() { - assert!(best_block.height > fork_point.height, - "Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height"); - *best_block = fork_point; - } - - // TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler. - // Internally this should call transaction_unconfirmed for all transactions that were - // confirmed at a height <= the one we now disconnected. - } -} - -impl< - ES: Deref + Clone, - NS: Deref + Clone, - CM: Deref + Clone, - C: Deref + Clone, - K: Deref + Clone, - TP: Deref + Clone, - T: Deref + Clone, - > Confirm for LiquidityManager -where - ES::Target: EntropySource, - NS::Target: NodeSigner, - CM::Target: AChannelManager, - C::Target: Filter, - K::Target: KVStore, - TP::Target: TimeProvider, - T::Target: BroadcasterInterface, -{ - fn transactions_confirmed( - &self, _header: &bitcoin::block::Header, _txdata: &chain::transaction::TransactionData, - _height: u32, - ) { - // TODO: Call transactions_confirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. - } - - fn transaction_unconfirmed(&self, _txid: &bitcoin::Txid) { - // TODO: Call transaction_unconfirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. - // Internally this should call transaction_unconfirmed for all transactions that were - // confirmed at a height <= the one we now unconfirmed. - } - - fn best_block_updated(&self, header: &bitcoin::block::Header, height: u32) { - let new_best_block = BestBlock::new(header.block_hash(), height); - *self.best_block.write().unwrap() = Some(new_best_block); - - // TODO: Call best_block_updated on all sub-modules that require it, e.g., LSPS1MessageHandler. - } - - fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option)> { - // TODO: Collect relevant txids from all sub-modules that, e.g., LSPS1MessageHandler. - Vec::new() - } -} - /// A synchroneous wrapper around [`LiquidityManager`] to be used in contexts where async is not /// available. pub struct LiquidityManagerSync< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, @@ -1073,12 +944,11 @@ pub struct LiquidityManagerSync< ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, { - inner: LiquidityManager, TP, T>, + inner: LiquidityManager, TP, T>, } #[cfg(feature = "time")] @@ -1086,25 +956,22 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, KS: Deref + Clone, T: Deref + Clone, - > LiquidityManagerSync + > LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, KS::Target: KVStoreSync, - C::Target: Filter, T::Target: BroadcasterInterface, { /// Constructor for the [`LiquidityManagerSync`] using the default system clock /// /// Wraps [`LiquidityManager::new`]. pub fn new( - entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option, - chain_params: Option, kv_store_sync: KS, transaction_broadcaster: T, - service_config: Option, + entropy_source: ES, node_signer: NS, channel_manager: CM, kv_store_sync: KS, + transaction_broadcaster: T, service_config: Option, client_config: Option, ) -> Result { let kv_store = KVStoreSyncWrapper(kv_store_sync); @@ -1113,8 +980,6 @@ where entropy_source, node_signer, channel_manager, - chain_source, - chain_params, kv_store, transaction_broadcaster, service_config, @@ -1138,16 +1003,14 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > LiquidityManagerSync + > LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -1156,9 +1019,8 @@ where /// /// Wraps [`LiquidityManager::new_with_custom_time_provider`]. pub fn new_with_custom_time_provider( - entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option, - chain_params: Option, kv_store_sync: KS, transaction_broadcaster: T, - service_config: Option, + entropy_source: ES, node_signer: NS, channel_manager: CM, kv_store_sync: KS, + transaction_broadcaster: T, service_config: Option, client_config: Option, time_provider: TP, ) -> Result { let kv_store = KVStoreSyncWrapper(kv_store_sync); @@ -1167,8 +1029,6 @@ where node_signer, channel_manager, transaction_broadcaster, - chain_source, - chain_params, kv_store, service_config, client_config, @@ -1308,16 +1168,14 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageReader for LiquidityManagerSync + > CustomMessageReader for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -1335,16 +1193,14 @@ impl< ES: Deref + Clone, NS: Deref + Clone, CM: Deref + Clone, - C: Deref + Clone, KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageHandler for LiquidityManagerSync + > CustomMessageHandler for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, - C::Target: Filter, KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, @@ -1377,71 +1233,3 @@ where self.inner.peer_connected(counterparty_node_id, init_msg, inbound) } } - -impl< - ES: Deref + Clone, - NS: Deref + Clone, - CM: Deref + Clone, - C: Deref + Clone, - KS: Deref + Clone, - TP: Deref + Clone, - T: Deref + Clone, - > Listen for LiquidityManagerSync -where - ES::Target: EntropySource, - NS::Target: NodeSigner, - CM::Target: AChannelManager, - C::Target: Filter, - KS::Target: KVStoreSync, - TP::Target: TimeProvider, - T::Target: BroadcasterInterface, -{ - fn filtered_block_connected( - &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - self.inner.filtered_block_connected(header, txdata, height) - } - - fn blocks_disconnected(&self, fork_point: BestBlock) { - self.inner.blocks_disconnected(fork_point); - } -} - -impl< - ES: Deref + Clone, - NS: Deref + Clone, - CM: Deref + Clone, - C: Deref + Clone, - KS: Deref + Clone, - TP: Deref + Clone, - T: Deref + Clone, - > Confirm for LiquidityManagerSync -where - ES::Target: EntropySource, - NS::Target: NodeSigner, - CM::Target: AChannelManager, - C::Target: Filter, - KS::Target: KVStoreSync, - TP::Target: TimeProvider, - T::Target: BroadcasterInterface, -{ - fn transactions_confirmed( - &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - self.inner.transactions_confirmed(header, txdata, height) - } - - fn transaction_unconfirmed(&self, txid: &bitcoin::Txid) { - self.inner.transaction_unconfirmed(txid) - } - - fn best_block_updated(&self, header: &bitcoin::block::Header, height: u32) { - self.inner.best_block_updated(header, height) - } - - fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option)> { - self.inner.get_relevant_txids() - } -} diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index dea987527ad..2716df7c0a3 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -3,13 +3,9 @@ use lightning_liquidity::utils::time::TimeProvider; use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig}; -use lightning::chain::{BestBlock, Filter}; -use lightning::ln::channelmanager::ChainParameters; use lightning::ln::functional_test_utils::{Node, TestChannelManager}; use lightning::util::test_utils::{TestBroadcaster, TestKeysInterface, TestStore}; -use bitcoin::Network; - use core::ops::Deref; use std::sync::Arc; @@ -26,11 +22,6 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( ) -> (LiquidityNode<'a, 'b, 'c>, LiquidityNode<'a, 'b, 'c>, Option>) { assert!(nodes.len() >= 2, "Need at least two nodes (service and client)"); - let chain_params = ChainParameters { - network: Network::Testnet, - best_block: BestBlock::from_network(Network::Testnet), - }; - let mut nodes_iter = nodes.into_iter(); let service_inner = nodes_iter.next().expect("missing service node"); let client_inner = nodes_iter.next().expect("missing client node"); @@ -40,8 +31,6 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( service_inner.keys_manager, service_inner.keys_manager, service_inner.node, - None::>, - Some(chain_params.clone()), service_kv_store, service_inner.tx_broadcaster, Some(service_config), @@ -54,8 +43,6 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( client_inner.keys_manager, client_inner.keys_manager, client_inner.node, - None::>, - Some(chain_params), client_kv_store, client_inner.tx_broadcaster, None, @@ -137,7 +124,6 @@ pub(crate) struct LiquidityNode<'a, 'b, 'c> { &'c TestKeysInterface, &'c TestKeysInterface, &'a TestChannelManager<'b, 'c>, - Arc, Arc, Arc, &'c TestBroadcaster, @@ -151,7 +137,6 @@ impl<'a, 'b, 'c> LiquidityNode<'a, 'b, 'c> { &'c TestKeysInterface, &'c TestKeysInterface, &'a TestChannelManager<'b, 'c>, - Arc, Arc, Arc, &'c TestBroadcaster, diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index e4ace27b715..2900302c0ea 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -28,8 +28,7 @@ use lightning_liquidity::lsps2::utils::is_valid_opening_fee_params; use lightning_liquidity::utils::time::{DefaultTimeProvider, TimeProvider}; use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig}; -use lightning::chain::{BestBlock, Filter}; -use lightning::ln::channelmanager::{ChainParameters, InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::functional_test_utils::{ create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs, }; @@ -1076,19 +1075,12 @@ fn lsps2_service_handler_persistence_across_restarts() { let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); // Create a new LiquidityManager with the same configuration and KV store to simulate restart - let chain_params = ChainParameters { - network: Network::Testnet, - best_block: BestBlock::from_network(Network::Testnet), - }; - let transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( nodes_restart[0].keys_manager, nodes_restart[0].keys_manager, nodes_restart[0].node, - None::>, - Some(chain_params), service_kv_store, transaction_broadcaster, Some(service_config), diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs index 16f20fd095f..6af0c137be5 100644 --- a/lightning-liquidity/tests/lsps5_integration_tests.rs +++ b/lightning-liquidity/tests/lsps5_integration_tests.rs @@ -7,9 +7,8 @@ use common::{ get_lsps_message, LSPSNodes, LiquidityNode, }; -use lightning::chain::{BestBlock, Filter}; use lightning::events::ClosureReason; -use lightning::ln::channelmanager::{ChainParameters, InterceptId}; +use lightning::ln::channelmanager::InterceptId; use lightning::ln::functional_test_utils::{ check_closed_event, close_channel, create_chan_between_nodes, create_chanmon_cfgs, create_network, create_node_cfgs, create_node_chanmgrs, Node, @@ -43,8 +42,6 @@ use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use lightning_types::payment::PaymentHash; -use bitcoin::Network; - use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -1601,18 +1598,10 @@ fn lsps5_service_handler_persistence_across_restarts() { let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); - // Create a new LiquidityManager with the same configuration and KV store to simulate restart - let chain_params = ChainParameters { - network: Network::Testnet, - best_block: BestBlock::from_network(Network::Testnet), - }; - let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( nodes_restart[0].keys_manager, nodes_restart[0].keys_manager, nodes_restart[0].node, - None::>, - Some(chain_params), service_kv_store, nodes_restart[0].tx_broadcaster, Some(service_config), From 86db784de3b908c70416c79575bfd807fb22de5d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 12:44:55 +0100 Subject: [PATCH 05/27] Move `PeerState` and related types to `peer_state.rs` module We move the `PeerState` related types to a new module. In the following commits we'll bit-by-bit drop the `pub(super)`s introduced here, asserting better separation of state and logic going forward. --- lightning-liquidity/src/lsps1/mod.rs | 2 + lightning-liquidity/src/lsps1/peer_state.rs | 84 +++++++++++++++++++++ lightning-liquidity/src/lsps1/service.rs | 65 +--------------- 3 files changed, 87 insertions(+), 64 deletions(-) create mode 100644 lightning-liquidity/src/lsps1/peer_state.rs diff --git a/lightning-liquidity/src/lsps1/mod.rs b/lightning-liquidity/src/lsps1/mod.rs index b068b186610..bdfc4045f54 100644 --- a/lightning-liquidity/src/lsps1/mod.rs +++ b/lightning-liquidity/src/lsps1/mod.rs @@ -13,4 +13,6 @@ pub mod client; pub mod event; pub mod msgs; #[cfg(lsps1_service)] +mod peer_state; +#[cfg(lsps1_service)] pub mod service; diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs new file mode 100644 index 00000000000..71eeb662120 --- /dev/null +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -0,0 +1,84 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains peer state objects that are used by `LSPS1ServiceHandler`. + +use super::msgs::{LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentInfo, LSPS1Request}; + +use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId}; +use crate::prelude::HashMap; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::util::logger::Level; + +#[derive(Default)] +pub(super) struct PeerState { + pub(super) outbound_channels_by_order_id: HashMap, + pub(super) pending_requests: HashMap, +} + +impl PeerState { + pub(super) fn insert_outbound_channel( + &mut self, order_id: LSPS1OrderId, channel: OutboundCRChannel, + ) { + self.outbound_channels_by_order_id.insert(order_id, channel); + } +} + +struct ChannelStateError(String); + +impl From for LightningError { + fn from(value: ChannelStateError) -> Self { + LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } + } +} + +#[derive(PartialEq, Debug)] +pub(super) enum OutboundRequestState { + OrderCreated { order_id: LSPS1OrderId }, + WaitingPayment { order_id: LSPS1OrderId }, +} + +impl OutboundRequestState { + fn awaiting_payment(&self) -> Result { + match self { + OutboundRequestState::OrderCreated { order_id } => { + Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) + }, + state => Err(ChannelStateError(format!("TODO. JIT Channel was in state: {:?}", state))), + } + } +} + +pub(super) struct OutboundLSPS1Config { + pub(super) order: LSPS1OrderParams, + pub(super) created_at: LSPSDateTime, + pub(super) payment: LSPS1PaymentInfo, +} + +pub(super) struct OutboundCRChannel { + pub(super) state: OutboundRequestState, + pub(super) config: OutboundLSPS1Config, +} + +impl OutboundCRChannel { + pub(super) fn new( + order: LSPS1OrderParams, created_at: LSPSDateTime, order_id: LSPS1OrderId, + payment: LSPS1PaymentInfo, + ) -> Self { + Self { + state: OutboundRequestState::OrderCreated { order_id }, + config: OutboundLSPS1Config { order, created_at, payment }, + } + } + pub(super) fn awaiting_payment(&mut self) -> Result<(), LightningError> { + self.state = self.state.awaiting_payment()?; + Ok(()) + } +} diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index ff37d2506e9..ba7067cc663 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -20,6 +20,7 @@ use super::msgs::{ LSPS1OrderState, LSPS1PaymentInfo, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, }; +use super::peer_state::{OutboundCRChannel, PeerState}; use crate::message_queue::MessageQueue; use crate::events::EventQueue; @@ -48,70 +49,6 @@ pub struct LSPS1ServiceConfig { pub supported_options: Option, } -struct ChannelStateError(String); - -impl From for LightningError { - fn from(value: ChannelStateError) -> Self { - LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } - } -} - -#[derive(PartialEq, Debug)] -enum OutboundRequestState { - OrderCreated { order_id: LSPS1OrderId }, - WaitingPayment { order_id: LSPS1OrderId }, -} - -impl OutboundRequestState { - fn awaiting_payment(&self) -> Result { - match self { - OutboundRequestState::OrderCreated { order_id } => { - Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) - }, - state => Err(ChannelStateError(format!("TODO. JIT Channel was in state: {:?}", state))), - } - } -} - -struct OutboundLSPS1Config { - order: LSPS1OrderParams, - created_at: LSPSDateTime, - payment: LSPS1PaymentInfo, -} - -struct OutboundCRChannel { - state: OutboundRequestState, - config: OutboundLSPS1Config, -} - -impl OutboundCRChannel { - fn new( - order: LSPS1OrderParams, created_at: LSPSDateTime, order_id: LSPS1OrderId, - payment: LSPS1PaymentInfo, - ) -> Self { - Self { - state: OutboundRequestState::OrderCreated { order_id }, - config: OutboundLSPS1Config { order, created_at, payment }, - } - } - fn awaiting_payment(&mut self) -> Result<(), LightningError> { - self.state = self.state.awaiting_payment()?; - Ok(()) - } -} - -#[derive(Default)] -struct PeerState { - outbound_channels_by_order_id: HashMap, - pending_requests: HashMap, -} - -impl PeerState { - fn insert_outbound_channel(&mut self, order_id: LSPS1OrderId, channel: OutboundCRChannel) { - self.outbound_channels_by_order_id.insert(order_id, channel); - } -} - /// The main object allowing to send and receive bLIP-51 / LSPS1 messages. pub struct LSPS1ServiceHandler where From 9aeb4daa4c6264f395281fd1bac60a2816cdee23 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 13:07:09 +0100 Subject: [PATCH 06/27] Drop bogus channel state handling .. we will re-add a proper state machine in a later commit, but for now we can just drop all of this half-baked logic that doesn't actually do anything. --- lightning-liquidity/src/lsps1/peer_state.rs | 41 +-------------------- lightning-liquidity/src/lsps1/service.rs | 23 ------------ 2 files changed, 2 insertions(+), 62 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 71eeb662120..3e9d17f4c73 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -14,9 +14,6 @@ use super::msgs::{LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentInfo, LSPS1Request use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId}; use crate::prelude::HashMap; -use lightning::ln::msgs::{ErrorAction, LightningError}; -use lightning::util::logger::Level; - #[derive(Default)] pub(super) struct PeerState { pub(super) outbound_channels_by_order_id: HashMap, @@ -31,31 +28,6 @@ impl PeerState { } } -struct ChannelStateError(String); - -impl From for LightningError { - fn from(value: ChannelStateError) -> Self { - LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } - } -} - -#[derive(PartialEq, Debug)] -pub(super) enum OutboundRequestState { - OrderCreated { order_id: LSPS1OrderId }, - WaitingPayment { order_id: LSPS1OrderId }, -} - -impl OutboundRequestState { - fn awaiting_payment(&self) -> Result { - match self { - OutboundRequestState::OrderCreated { order_id } => { - Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) - }, - state => Err(ChannelStateError(format!("TODO. JIT Channel was in state: {:?}", state))), - } - } -} - pub(super) struct OutboundLSPS1Config { pub(super) order: LSPS1OrderParams, pub(super) created_at: LSPSDateTime, @@ -63,22 +35,13 @@ pub(super) struct OutboundLSPS1Config { } pub(super) struct OutboundCRChannel { - pub(super) state: OutboundRequestState, pub(super) config: OutboundLSPS1Config, } impl OutboundCRChannel { pub(super) fn new( - order: LSPS1OrderParams, created_at: LSPSDateTime, order_id: LSPS1OrderId, - payment: LSPS1PaymentInfo, + order: LSPS1OrderParams, created_at: LSPSDateTime, payment: LSPS1PaymentInfo, ) -> Self { - Self { - state: OutboundRequestState::OrderCreated { order_id }, - config: OutboundLSPS1Config { order, created_at, payment }, - } - } - pub(super) fn awaiting_payment(&mut self) -> Result<(), LightningError> { - self.state = self.state.awaiting_payment()?; - Ok(()) + Self { config: OutboundLSPS1Config { order, created_at, payment } } } } diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index ba7067cc663..1a5c364a390 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -198,7 +198,6 @@ where let channel = OutboundCRChannel::new( params.order.clone(), created_at, - order_id.clone(), payment.clone(), ); @@ -237,28 +236,6 @@ where match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - - let outbound_channel = peer_state_lock - .outbound_channels_by_order_id - .get_mut(¶ms.order_id) - .ok_or(LightningError { - err: format!( - "Received get order request for unknown order id {:?}", - params.order_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - if let Err(e) = outbound_channel.awaiting_payment() { - peer_state_lock.outbound_channels_by_order_id.remove(¶ms.order_id); - event_queue_notifier.enqueue(LSPS1ServiceEvent::Refund { - request_id, - counterparty_node_id: *counterparty_node_id, - order_id: params.order_id, - }); - return Err(e); - } - peer_state_lock .pending_requests .insert(request_id.clone(), LSPS1Request::GetOrder(params.clone())); From b42592dc99e24152dd392912cb790ae3f1da6309 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 13:01:51 +0100 Subject: [PATCH 07/27] Replace `insert_outbound_channel` with `PeerState::new_order` .. requiring less access to internals --- lightning-liquidity/src/lsps1/peer_state.rs | 7 +++++-- lightning-liquidity/src/lsps1/service.rs | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 3e9d17f4c73..729d6827330 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -21,9 +21,12 @@ pub(super) struct PeerState { } impl PeerState { - pub(super) fn insert_outbound_channel( - &mut self, order_id: LSPS1OrderId, channel: OutboundCRChannel, + pub(super) fn new_order( + &mut self, order_id: LSPS1OrderId, order_params: LSPS1OrderParams, + created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, ) { + let channel = OutboundCRChannel::new(order_params, created_at, payment_details); + self.outbound_channels_by_order_id.insert(order_id, channel); } } diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 1a5c364a390..9bb153fcbaa 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -20,7 +20,7 @@ use super::msgs::{ LSPS1OrderState, LSPS1PaymentInfo, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, }; -use super::peer_state::{OutboundCRChannel, PeerState}; +use super::peer_state::PeerState; use crate::message_queue::MessageQueue; use crate::events::EventQueue; @@ -195,14 +195,14 @@ where match peer_state_lock.pending_requests.remove(&request_id) { Some(LSPS1Request::CreateOrder(params)) => { let order_id = self.generate_order_id(); - let channel = OutboundCRChannel::new( + + peer_state_lock.new_order( + order_id.clone(), params.order.clone(), created_at, payment.clone(), ); - peer_state_lock.insert_outbound_channel(order_id.clone(), channel); - let response = LSPS1Response::CreateOrder(LSPS1CreateOrderResponse { order: params.order, order_id, From ac544ee38024a78099daa23ac8adc171f5c93cf4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 13:35:36 +0100 Subject: [PATCH 08/27] Use `PeerState::{get_order, has_active_requests}` instead of map Previously, we'd directly access the internal `outbound_` map of `PeerState`. Here we refactor the code to avoid this. Note this also highlighted a bug in that we currently don't actually update/persist the order state in `update_order_state`. We don't fix this here, but just improve isolation for now, as all state update behavior will be reworked later. --- lightning-liquidity/src/lsps1/peer_state.rs | 27 ++++++++----- lightning-liquidity/src/lsps1/service.rs | 43 ++++++++++----------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 729d6827330..9b79d25fa07 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -16,7 +16,7 @@ use crate::prelude::HashMap; #[derive(Default)] pub(super) struct PeerState { - pub(super) outbound_channels_by_order_id: HashMap, + outbound_channels_by_order_id: HashMap, pub(super) pending_requests: HashMap, } @@ -26,25 +26,32 @@ impl PeerState { created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, ) { let channel = OutboundCRChannel::new(order_params, created_at, payment_details); - self.outbound_channels_by_order_id.insert(order_id, channel); } + + pub(super) fn get_order<'a>(&'a self, order_id: &LSPS1OrderId) -> Option<&'a ChannelOrder> { + self.outbound_channels_by_order_id.get(order_id).map(|channel| &channel.order) + } + + pub(super) fn has_active_requests(&self) -> bool { + !self.outbound_channels_by_order_id.is_empty() + } } -pub(super) struct OutboundLSPS1Config { - pub(super) order: LSPS1OrderParams, +pub(super) struct ChannelOrder { + pub(super) order_params: LSPS1OrderParams, pub(super) created_at: LSPSDateTime, - pub(super) payment: LSPS1PaymentInfo, + pub(super) payment_details: LSPS1PaymentInfo, } -pub(super) struct OutboundCRChannel { - pub(super) config: OutboundLSPS1Config, +struct OutboundCRChannel { + order: ChannelOrder, } impl OutboundCRChannel { - pub(super) fn new( - order: LSPS1OrderParams, created_at: LSPSDateTime, payment: LSPS1PaymentInfo, + fn new( + order_params: LSPS1OrderParams, created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, ) -> Self { - Self { config: OutboundLSPS1Config { order, created_at, payment } } + Self { order: ChannelOrder { order_params, created_at, payment_details } } } } diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 9bb153fcbaa..6dc192bd8fd 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -101,7 +101,7 @@ where let outer_state_lock = self.per_peer_state.read().unwrap(); outer_state_lock.get(counterparty_node_id).map_or(false, |inner| { let peer_state = inner.lock().unwrap(); - !peer_state.outbound_channels_by_order_id.is_empty() + peer_state.has_active_requests() }) } @@ -275,29 +275,26 @@ where match outer_state_lock.get(&counterparty_node_id) { Some(inner_state_lock) => { - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - - if let Some(outbound_channel) = - peer_state_lock.outbound_channels_by_order_id.get_mut(&order_id) - { - let config = &outbound_channel.config; - - let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { - order_id, - order: config.order.clone(), - order_state, - created_at: config.created_at.clone(), - payment: config.payment.clone(), - channel, - }); - let msg = LSPS1Message::Response(request_id, response).into(); - message_queue_notifier.enqueue(&counterparty_node_id, msg); - Ok(()) - } else { - Err(APIError::APIMisuseError { + let peer_state_lock = inner_state_lock.lock().unwrap(); + let order = + peer_state_lock.get_order(&order_id).ok_or(APIError::APIMisuseError { err: format!("Channel with order_id {} not found", order_id.0), - }) - } + })?; + + // FIXME: we need to actually remember the order state (and eventually persist it) + // here. + + let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { + order_id, + order: order.order_params.clone(), + order_state, + created_at: order.created_at.clone(), + payment: order.payment_details.clone(), + channel, + }); + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + Ok(()) }, None => Err(APIError::APIMisuseError { err: format!("No existing state with counterparty {}", counterparty_node_id), From 0752a5567115eecb5045ef24fbd18bb7fd67bbff Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 14:11:22 +0100 Subject: [PATCH 09/27] Use `PeerState::{register,remove}_request` instead of map access We introduce two new methods on `PeerState` to avoid direct access to the internal `pending_requests` map. --- lightning-liquidity/src/lsps1/peer_state.rs | 35 +++++++++++++- lightning-liquidity/src/lsps1/service.rs | 52 +++++++++++++++------ 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 9b79d25fa07..70ddd12fc06 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -14,10 +14,12 @@ use super::msgs::{LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentInfo, LSPS1Request use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId}; use crate::prelude::HashMap; +use core::fmt; + #[derive(Default)] pub(super) struct PeerState { outbound_channels_by_order_id: HashMap, - pub(super) pending_requests: HashMap, + pending_requests: HashMap, } impl PeerState { @@ -33,11 +35,42 @@ impl PeerState { self.outbound_channels_by_order_id.get(order_id).map(|channel| &channel.order) } + pub(super) fn register_request( + &mut self, request_id: LSPSRequestId, request: LSPS1Request, + ) -> Result<(), PeerStateError> { + if self.pending_requests.contains_key(&request_id) { + return Err(PeerStateError::DuplicateRequestId); + } + self.pending_requests.insert(request_id, request); + Ok(()) + } + + pub(super) fn remove_request( + &mut self, request_id: &LSPSRequestId, + ) -> Result { + self.pending_requests.remove(request_id).ok_or(PeerStateError::UnknownRequestId) + } + pub(super) fn has_active_requests(&self) -> bool { !self.outbound_channels_by_order_id.is_empty() } } +#[derive(Debug, Copy, Clone)] +pub(super) enum PeerStateError { + UnknownRequestId, + DuplicateRequestId, +} + +impl fmt::Display for PeerStateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownRequestId => write!(f, "unknown request id"), + Self::DuplicateRequestId => write!(f, "duplicate request id"), + } + } +} + pub(super) struct ChannelOrder { pub(super) order_params: LSPS1OrderParams, pub(super) created_at: LSPSDateTime, diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 6dc192bd8fd..de6304af1b0 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -162,9 +162,12 @@ where .or_insert(Mutex::new(PeerState::default())); let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS1Request::CreateOrder(params.clone())); + let request = LSPS1Request::CreateOrder(params.clone()); + peer_state_lock.register_request(request_id.clone(), request).map_err(|e| { + let err = format!("Failed to handle request due to: {}", e); + let action = ErrorAction::IgnoreAndLog(Level::Error); + LightningError { err, action } + })?; } event_queue_notifier.enqueue(LSPS1ServiceEvent::RequestForPaymentDetails { @@ -191,11 +194,15 @@ where match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - - match peer_state_lock.pending_requests.remove(&request_id) { - Some(LSPS1Request::CreateOrder(params)) => { + let request = peer_state_lock.remove_request(&request_id).map_err(|e| { + debug_assert!(false, "Failed to send response due to: {}", e); + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + + match request { + LSPS1Request::CreateOrder(params) => { let order_id = self.generate_order_id(); - peer_state_lock.new_order( order_id.clone(), params.order.clone(), @@ -206,6 +213,9 @@ where let response = LSPS1Response::CreateOrder(LSPS1CreateOrderResponse { order: params.order, order_id, + + // TODO, we need to set this in the peer/channel state, and send the + // set value here: order_state: LSPS1OrderState::Created, created_at, payment, @@ -215,14 +225,22 @@ where message_queue_notifier.enqueue(counterparty_node_id, msg); Ok(()) }, - - _ => Err(APIError::APIMisuseError { - err: format!("No pending buy request for request_id: {:?}", request_id), - }), + t => { + debug_assert!( + false, + "Failed to send response due to unexpected request type: {:?}", + t + ); + let err = format!( + "Failed to send response due to unexpected request type: {:?}", + t + ); + return Err(APIError::APIMisuseError { err }); + }, } }, None => Err(APIError::APIMisuseError { - err: format!("No state for the counterparty exists: {:?}", counterparty_node_id), + err: format!("No state for the counterparty exists: {}", counterparty_node_id), }), } } @@ -236,9 +254,13 @@ where match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS1Request::GetOrder(params.clone())); + + let request = LSPS1Request::GetOrder(params.clone()); + peer_state_lock.register_request(request_id.clone(), request).map_err(|e| { + let err = format!("Failed to handle request due to: {}", e); + let action = ErrorAction::IgnoreAndLog(Level::Error); + LightningError { err, action } + })?; event_queue_notifier.enqueue(LSPS1ServiceEvent::CheckPaymentConfirmation { request_id, From eb3f007ecf110c413dab6637e055c14164eefd4a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 14:16:00 +0100 Subject: [PATCH 10/27] Drop `OutboundCRChannel` The `OutboundChannel` construct simply wrapped `ChannelOrder` which we can now simply use directly. --- lightning-liquidity/src/lsps1/peer_state.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 70ddd12fc06..90700795365 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -18,7 +18,7 @@ use core::fmt; #[derive(Default)] pub(super) struct PeerState { - outbound_channels_by_order_id: HashMap, + outbound_channels_by_order_id: HashMap, pending_requests: HashMap, } @@ -27,12 +27,12 @@ impl PeerState { &mut self, order_id: LSPS1OrderId, order_params: LSPS1OrderParams, created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, ) { - let channel = OutboundCRChannel::new(order_params, created_at, payment_details); - self.outbound_channels_by_order_id.insert(order_id, channel); + let channel_order = ChannelOrder { order_params, created_at, payment_details }; + self.outbound_channels_by_order_id.insert(order_id, channel_order); } pub(super) fn get_order<'a>(&'a self, order_id: &LSPS1OrderId) -> Option<&'a ChannelOrder> { - self.outbound_channels_by_order_id.get(order_id).map(|channel| &channel.order) + self.outbound_channels_by_order_id.get(order_id) } pub(super) fn register_request( @@ -76,15 +76,3 @@ pub(super) struct ChannelOrder { pub(super) created_at: LSPSDateTime, pub(super) payment_details: LSPS1PaymentInfo, } - -struct OutboundCRChannel { - order: ChannelOrder, -} - -impl OutboundCRChannel { - fn new( - order_params: LSPS1OrderParams, created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, - ) -> Self { - Self { order: ChannelOrder { order_params, created_at, payment_details } } - } -} From cdac370afbc7f4f99f180e2bde93438be4a65acd Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 16 Nov 2025 14:44:46 +0100 Subject: [PATCH 11/27] Actually remember the order state in `ChannelOrder` We here remember and update the order state and channel details in `ChannelOrder` --- lightning-liquidity/src/lsps1/peer_state.rs | 38 ++++++++++++++++---- lightning-liquidity/src/lsps1/service.rs | 40 ++++++++++----------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 90700795365..d6cedb13c6c 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -9,7 +9,10 @@ //! Contains peer state objects that are used by `LSPS1ServiceHandler`. -use super::msgs::{LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentInfo, LSPS1Request}; +use super::msgs::{ + LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1OrderState, LSPS1PaymentInfo, + LSPS1Request, +}; use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId}; use crate::prelude::HashMap; @@ -26,13 +29,31 @@ impl PeerState { pub(super) fn new_order( &mut self, order_id: LSPS1OrderId, order_params: LSPS1OrderParams, created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, - ) { - let channel_order = ChannelOrder { order_params, created_at, payment_details }; - self.outbound_channels_by_order_id.insert(order_id, channel_order); + ) -> ChannelOrder { + let order_state = LSPS1OrderState::Created; + let channel_details = None; + let channel_order = ChannelOrder { + order_params, + order_state, + created_at, + payment_details, + channel_details, + }; + self.outbound_channels_by_order_id.insert(order_id, channel_order.clone()); + channel_order } - pub(super) fn get_order<'a>(&'a self, order_id: &LSPS1OrderId) -> Option<&'a ChannelOrder> { - self.outbound_channels_by_order_id.get(order_id) + pub(super) fn update_order<'a>( + &'a mut self, order_id: &LSPS1OrderId, order_state: LSPS1OrderState, + channel_details: Option, + ) -> Result<&'a ChannelOrder, PeerStateError> { + let order = self + .outbound_channels_by_order_id + .get_mut(order_id) + .ok_or(PeerStateError::UnknownOrderId)?; + order.order_state = order_state; + order.channel_details = channel_details; + Ok(order) } pub(super) fn register_request( @@ -60,6 +81,7 @@ impl PeerState { pub(super) enum PeerStateError { UnknownRequestId, DuplicateRequestId, + UnknownOrderId, } impl fmt::Display for PeerStateError { @@ -67,12 +89,16 @@ impl fmt::Display for PeerStateError { match self { Self::UnknownRequestId => write!(f, "unknown request id"), Self::DuplicateRequestId => write!(f, "duplicate request id"), + Self::UnknownOrderId => write!(f, "unknown order id"), } } } +#[derive(Debug, Clone)] pub(super) struct ChannelOrder { pub(super) order_params: LSPS1OrderParams, + pub(super) order_state: LSPS1OrderState, pub(super) created_at: LSPSDateTime, pub(super) payment_details: LSPS1PaymentInfo, + pub(super) channel_details: Option, } diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index de6304af1b0..b2f3a3bcba4 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -186,7 +186,7 @@ where /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails pub fn send_payment_details( &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, - payment: LSPS1PaymentInfo, created_at: LSPSDateTime, + payment_details: LSPS1PaymentInfo, created_at: LSPSDateTime, ) -> Result<(), APIError> { let mut message_queue_notifier = self.pending_messages.notifier(); @@ -203,23 +203,21 @@ where match request { LSPS1Request::CreateOrder(params) => { let order_id = self.generate_order_id(); - peer_state_lock.new_order( + let order = peer_state_lock.new_order( order_id.clone(), - params.order.clone(), + params.order, created_at, - payment.clone(), + payment_details, ); let response = LSPS1Response::CreateOrder(LSPS1CreateOrderResponse { - order: params.order, + order: order.order_params, order_id, - // TODO, we need to set this in the peer/channel state, and send the - // set value here: - order_state: LSPS1OrderState::Created, - created_at, - payment, - channel: None, + order_state: order.order_state, + created_at: order.created_at, + payment: order.payment_details, + channel: order.channel_details, }); let msg = LSPS1Message::Response(request_id, response).into(); message_queue_notifier.enqueue(counterparty_node_id, msg); @@ -289,7 +287,7 @@ where /// [`LSPS1ServiceEvent::CheckPaymentConfirmation`]: crate::lsps1::event::LSPS1ServiceEvent::CheckPaymentConfirmation pub fn update_order_status( &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, - order_state: LSPS1OrderState, channel: Option, + order_state: LSPS1OrderState, channel_details: Option, ) -> Result<(), APIError> { let mut message_queue_notifier = self.pending_messages.notifier(); @@ -297,22 +295,20 @@ where match outer_state_lock.get(&counterparty_node_id) { Some(inner_state_lock) => { - let peer_state_lock = inner_state_lock.lock().unwrap(); - let order = - peer_state_lock.get_order(&order_id).ok_or(APIError::APIMisuseError { - err: format!("Channel with order_id {} not found", order_id.0), - })?; - - // FIXME: we need to actually remember the order state (and eventually persist it) - // here. + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + let order = peer_state_lock + .update_order(&order_id, order_state, channel_details) + .map_err(|e| APIError::APIMisuseError { + err: format!("Failed to update order: {:?}", e), + })?; let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { order_id, order: order.order_params.clone(), - order_state, + order_state: order.order_state.clone(), created_at: order.created_at.clone(), payment: order.payment_details.clone(), - channel, + channel: order.channel_details.clone(), }); let msg = LSPS1Message::Response(request_id, response).into(); message_queue_notifier.enqueue(&counterparty_node_id, msg); From bb98b802b50c1fdcafd1fc00faf842ac97959e5e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 10 Dec 2025 09:49:46 +0100 Subject: [PATCH 12/27] `LSPS1ServiceHandler`: Use `TimeProvider` when creating new orders Since we by now have the `TimeProvider` trait, we might as well use it in `LSPS1ServiceHandler` instead of requiring the user to provide a `created_at` manually. --- lightning-liquidity/src/lsps1/service.rs | 23 ++++++++++++++----- lightning-liquidity/src/manager.rs | 11 +++++---- .../tests/lsps1_integration_tests.rs | 8 ++----- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index b2f3a3bcba4..bf567981ee2 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -30,6 +30,7 @@ use crate::lsps0::ser::{ use crate::prelude::{new_hash_map, HashMap}; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils; +use crate::utils::time::TimeProvider; use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; @@ -50,31 +51,36 @@ pub struct LSPS1ServiceConfig { } /// The main object allowing to send and receive bLIP-51 / LSPS1 messages. -pub struct LSPS1ServiceHandler +pub struct LSPS1ServiceHandler where ES::Target: EntropySource, CM::Target: AChannelManager, K::Target: KVStore, + TP::Target: TimeProvider, { entropy_source: ES, _channel_manager: CM, pending_messages: Arc, pending_events: Arc>, per_peer_state: RwLock>>, + time_provider: TP, config: LSPS1ServiceConfig, } -impl LSPS1ServiceHandler +impl + LSPS1ServiceHandler where ES::Target: EntropySource, CM::Target: AChannelManager, ES::Target: EntropySource, K::Target: KVStore, + TP::Target: TimeProvider, { /// Constructs a `LSPS1ServiceHandler`. pub(crate) fn new( entropy_source: ES, pending_messages: Arc, - pending_events: Arc>, channel_manager: CM, config: LSPS1ServiceConfig, + pending_events: Arc>, channel_manager: CM, time_provider: TP, + config: LSPS1ServiceConfig, ) -> Self { Self { entropy_source, @@ -82,6 +88,7 @@ where pending_messages, pending_events, per_peer_state: RwLock::new(new_hash_map()), + time_provider, config, } } @@ -186,7 +193,7 @@ where /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails pub fn send_payment_details( &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, - payment_details: LSPS1PaymentInfo, created_at: LSPSDateTime, + payment_details: LSPS1PaymentInfo, ) -> Result<(), APIError> { let mut message_queue_notifier = self.pending_messages.notifier(); @@ -203,6 +210,9 @@ where match request { LSPS1Request::CreateOrder(params) => { let order_id = self.generate_order_id(); + let created_at = LSPSDateTime::new_from_duration_since_epoch( + self.time_provider.duration_since_epoch(), + ); let order = peer_state_lock.new_order( order_id.clone(), params.order, @@ -326,12 +336,13 @@ where } } -impl LSPSProtocolMessageHandler - for LSPS1ServiceHandler +impl LSPSProtocolMessageHandler + for LSPS1ServiceHandler where ES::Target: EntropySource, CM::Target: AChannelManager, K::Target: KVStore, + TP::Target: TimeProvider, { type ProtocolMessage = LSPS1Message; const PROTOCOL_NUMBER: Option = Some(1); diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 1116f7d27e4..7075a10f845 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -300,7 +300,7 @@ pub struct LiquidityManager< lsps0_client_handler: LSPS0ClientHandler, lsps0_service_handler: Option, #[cfg(lsps1_service)] - lsps1_service_handler: Option>, + lsps1_service_handler: Option>, lsps1_client_handler: Option>, lsps2_service_handler: Option>, lsps2_client_handler: Option>, @@ -454,7 +454,7 @@ where kv_store.clone(), node_signer, lsps5_service_config.clone(), - time_provider, + time_provider.clone(), )) } else { None @@ -477,7 +477,7 @@ where #[cfg(lsps1_service)] let lsps1_service_handler = service_config.as_ref().and_then(|config| { if let Some(number) = - as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER + as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER { supported_protocols.push(number); } @@ -487,6 +487,7 @@ where Arc::clone(&pending_messages), Arc::clone(&pending_events), channel_manager.clone(), + time_provider, config.clone(), ) }) @@ -544,7 +545,7 @@ where /// Returns a reference to the LSPS1 server-side handler. #[cfg(lsps1_service)] - pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler> { + pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler> { self.lsps1_service_handler.as_ref() } @@ -1074,7 +1075,7 @@ where #[cfg(lsps1_service)] pub fn lsps1_service_handler( &self, - ) -> Option<&LSPS1ServiceHandler>> { + ) -> Option<&LSPS1ServiceHandler, TP>> { self.inner.lsps1_service_handler() } diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index 5e842c6a111..0db96f591e9 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -7,7 +7,6 @@ use common::{get_lsps_message, LSPSNodes}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning_liquidity::events::LiquidityEvent; -use lightning_liquidity::lsps0::ser::LSPSDateTime; use lightning_liquidity::lsps1::client::LSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; use lightning_liquidity::lsps1::event::LSPS1ServiceEvent; @@ -24,7 +23,6 @@ use lightning::ln::functional_test_utils::{ }; use lightning::util::test_utils::TestStore; -use std::str::FromStr; use std::sync::Arc; use lightning::ln::functional_test_utils::{create_network, Node}; @@ -177,10 +175,8 @@ fn lsps1_happy_path() { let onchain: LSPS1OnchainPaymentInfo = serde_json::from_str(json_str).expect("Failed to parse JSON"); let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; - let _now = LSPSDateTime::from_str("2024-01-01T00:00:00Z").expect("Failed to parse date"); - - let _ = service_handler - .send_payment_details(_create_order_id.clone(), &client_node_id, payment_info.clone(), _now) + service_handler + .send_payment_details(_create_order_id.clone(), &client_node_id, payment_info.clone()) .unwrap(); let create_order_response = get_lsps_message!(service_node, client_node_id); From f607f92bc3f98aa9c5a88568d8a5bf2d6db38533 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 10 Dec 2025 10:35:12 +0100 Subject: [PATCH 13/27] Require `supported_options` in `LSPS1ServiceConfig` In the future we might want to inline the fields in `LSPS1ServiceConfig` (especially once some are added that we'd want to always/never set for the user), but for now we just make the `supported_options` field in `LSPS1ServiceConfig` required, avoiding some dangerous `unwrap`s. --- lightning-liquidity/src/lsps1/service.rs | 19 ++++--------------- .../tests/lsps0_integration_tests.rs | 18 +++++++++++++++++- .../tests/lsps1_integration_tests.rs | 3 +-- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index bf567981ee2..f6dd0720e5c 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -47,7 +47,7 @@ pub struct LSPS1ServiceConfig { /// A token to be send with each channel request. pub token: Option, /// The options supported by the LSP. - pub supported_options: Option, + pub supported_options: LSPS1Options, } /// The main object allowing to send and receive bLIP-51 / LSPS1 messages. @@ -118,15 +118,7 @@ where let mut message_queue_notifier = self.pending_messages.notifier(); let response = LSPS1Response::GetInfo(LSPS1GetInfoResponse { - options: self - .config - .supported_options - .clone() - .ok_or(LightningError { - err: format!("Configuration for LSP server not set."), - action: ErrorAction::IgnoreAndLog(Level::Info), - }) - .unwrap(), + options: self.config.supported_options.clone(), }); let msg = LSPS1Message::Response(request_id, response).into(); @@ -141,14 +133,11 @@ where let mut message_queue_notifier = self.pending_messages.notifier(); let event_queue_notifier = self.pending_events.notifier(); - if !is_valid(¶ms.order, &self.config.supported_options.as_ref().unwrap()) { + if !is_valid(¶ms.order, &self.config.supported_options) { let response = LSPS1Response::CreateOrderError(LSPSResponseError { code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, message: format!("Order does not match options supported by LSP server"), - data: Some(format!( - "Supported options are {:?}", - &self.config.supported_options.as_ref().unwrap() - )), + data: Some(format!("Supported options are {:?}", &self.config.supported_options)), }); let msg = LSPS1Message::Response(request_id, response).into(); message_queue_notifier.enqueue(counterparty_node_id, msg); diff --git a/lightning-liquidity/tests/lsps0_integration_tests.rs b/lightning-liquidity/tests/lsps0_integration_tests.rs index 423d49785f2..7f0e01bde92 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -9,6 +9,8 @@ use lightning_liquidity::lsps0::event::LSPS0ClientEvent; #[cfg(lsps1_service)] use lightning_liquidity::lsps1::client::LSPS1ClientConfig; #[cfg(lsps1_service)] +use lightning_liquidity::lsps1::msgs::LSPS1Options; +#[cfg(lsps1_service)] use lightning_liquidity::lsps1::service::LSPS1ServiceConfig; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; @@ -34,7 +36,21 @@ fn list_protocols_integration_test() { let promise_secret = [42; 32]; let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; #[cfg(lsps1_service)] - let lsps1_service_config = LSPS1ServiceConfig { supported_options: None, token: None }; + let lsps1_service_config = { + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + LSPS1ServiceConfig { supported_options, token: None } + }; let lsps5_service_config = LSPS5ServiceConfig::default(); let service_config = LiquidityServiceConfig { #[cfg(lsps1_service)] diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index 0db96f591e9..e799cced976 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -30,8 +30,7 @@ use lightning::ln::functional_test_utils::{create_network, Node}; fn build_lsps1_configs( supported_options: LSPS1Options, ) -> (LiquidityServiceConfig, LiquidityClientConfig) { - let lsps1_service_config = - LSPS1ServiceConfig { token: None, supported_options: Some(supported_options) }; + let lsps1_service_config = LSPS1ServiceConfig { token: None, supported_options }; let service_config = LiquidityServiceConfig { lsps1_service_config: Some(lsps1_service_config), lsps2_service_config: None, From 2e358adf6eb18976c837a0ee245d938e045cd5f0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 10 Dec 2025 11:13:22 +0100 Subject: [PATCH 14/27] Remove pending request in `update_order_status` .. otherwise we'd keep the request around forever. --- lightning-liquidity/src/lsps1/service.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index f6dd0720e5c..d5e9ba2b895 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -301,6 +301,12 @@ where err: format!("Failed to update order: {:?}", e), })?; + peer_state_lock.remove_request(&request_id).map_err(|e| { + debug_assert!(false, "Failed to send response due to: {}", e); + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { order_id, order: order.order_params.clone(), From 4ecb29edf0bf88e88956b87a4e827de55a510084 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 10 Dec 2025 11:14:53 +0100 Subject: [PATCH 15/27] Minor cleanups and typo fixes --- lightning-liquidity/src/lsps1/service.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index d5e9ba2b895..115d18817d1 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -72,7 +72,6 @@ impl where ES::Target: EntropySource, CM::Target: AChannelManager, - ES::Target: EntropySource, K::Target: KVStore, TP::Target: TimeProvider, { @@ -277,8 +276,9 @@ where } /// Used by LSP to give details to client regarding the status of channel opening. - /// Called to respond to client's GetOrder request. - /// The LSP continously polls for checking payment confirmation on-chain or lighting + /// Called to respond to client's `GetOrder` request. + /// + /// The LSP continously polls for checking payment confirmation on-chain or Lightning /// and then responds to client request. /// /// Should be called in response to receiving a [`LSPS1ServiceEvent::CheckPaymentConfirmation`] event. @@ -373,7 +373,7 @@ fn check_range(min: u64, max: u64, value: u64) -> bool { } fn is_valid(order: &LSPS1OrderParams, options: &LSPS1Options) -> bool { - let bool = check_range( + check_range( options.min_initial_client_balance_sat, options.max_initial_client_balance_sat, order.client_balance_sat, @@ -385,7 +385,5 @@ fn is_valid(order: &LSPS1OrderParams, options: &LSPS1Options) -> bool { 1, options.max_channel_expiry_blocks.into(), order.channel_expiry_blocks.into(), - ); - - bool + ) } From 0a66771601b3e073f2c8eb2d78f8ef7b18b412bd Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 11 Dec 2025 09:26:31 +0100 Subject: [PATCH 16/27] Respond to `GetOrder` requests from our saved state Previously, we'd use an event to have the user check the order status and then call back in. As we already track the order status, we here change that to a model where we respond immediately based on our state and have the user/LSP update that state whenever it detects a change (e.g., a received payment, reorg, etc.). In the next commmit we will add/modify the corresponding API methods to do so. --- lightning-liquidity/src/lsps1/event.rs | 20 ----- lightning-liquidity/src/lsps1/msgs.rs | 2 + lightning-liquidity/src/lsps1/peer_state.rs | 14 +++- lightning-liquidity/src/lsps1/service.rs | 82 +++++++++---------- .../tests/lsps1_integration_tests.rs | 26 ------ 5 files changed, 54 insertions(+), 90 deletions(-) diff --git a/lightning-liquidity/src/lsps1/event.rs b/lightning-liquidity/src/lsps1/event.rs index fdf3fc57b0d..d966f8bdc2f 100644 --- a/lightning-liquidity/src/lsps1/event.rs +++ b/lightning-liquidity/src/lsps1/event.rs @@ -165,26 +165,6 @@ pub enum LSPS1ServiceEvent { /// The order requested by the client. order: LSPS1OrderParams, }, - /// A request from client to check the status of the payment. - /// - /// An event to poll for checking payment status either onchain or lightning. - /// - /// You must call [`LSPS1ServiceHandler::update_order_status`] to update the client - /// regarding the status of the payment and order. - /// - /// **Note: ** This event will *not* be persisted across restarts. - /// - /// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status - CheckPaymentConfirmation { - /// An identifier that must be passed to [`LSPS1ServiceHandler::update_order_status`]. - /// - /// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status - request_id: LSPSRequestId, - /// The node id of the client making the information request. - counterparty_node_id: PublicKey, - /// The order id of order with pending payment. - order_id: LSPS1OrderId, - }, /// If error is encountered, refund the amount if paid by the client. /// /// **Note: ** This event will *not* be persisted across restarts. diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs index 8402827a4a6..4f79a13821a 100644 --- a/lightning-liquidity/src/lsps1/msgs.rs +++ b/lightning-liquidity/src/lsps1/msgs.rs @@ -32,6 +32,8 @@ pub(crate) const LSPS1_GET_ORDER_METHOD_NAME: &str = "lsps1.get_order"; pub(crate) const _LSPS1_CREATE_ORDER_REQUEST_INVALID_PARAMS_ERROR_CODE: i32 = -32602; #[cfg(lsps1_service)] pub(crate) const LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE: i32 = 100; +#[cfg(lsps1_service)] +pub(crate) const LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE: i32 = 101; /// The identifier of an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index d6cedb13c6c..83b4b2f4d5b 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -43,17 +43,27 @@ impl PeerState { channel_order } + pub(super) fn get_order<'a>( + &'a self, order_id: &LSPS1OrderId, + ) -> Result<&'a ChannelOrder, PeerStateError> { + let order = self + .outbound_channels_by_order_id + .get(order_id) + .ok_or(PeerStateError::UnknownOrderId)?; + Ok(order) + } + pub(super) fn update_order<'a>( &'a mut self, order_id: &LSPS1OrderId, order_state: LSPS1OrderState, channel_details: Option, - ) -> Result<&'a ChannelOrder, PeerStateError> { + ) -> Result<(), PeerStateError> { let order = self .outbound_channels_by_order_id .get_mut(order_id) .ok_or(PeerStateError::UnknownOrderId)?; order.order_state = order_state; order.channel_details = channel_details; - Ok(order) + Ok(()) } pub(super) fn register_request( diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 115d18817d1..218feefe5c0 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -19,6 +19,7 @@ use super::msgs::{ LSPS1GetOrderRequest, LSPS1Message, LSPS1Options, LSPS1OrderId, LSPS1OrderParams, LSPS1OrderState, LSPS1PaymentInfo, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, + LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, }; use super::peer_state::PeerState; use crate::message_queue::MessageQueue; @@ -245,78 +246,75 @@ where &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, params: LSPS1GetOrderRequest, ) -> Result<(), LightningError> { - let event_queue_notifier = self.pending_events.notifier(); + let mut message_queue_notifier = self.pending_messages.notifier(); let outer_state_lock = self.per_peer_state.read().unwrap(); match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - - let request = LSPS1Request::GetOrder(params.clone()); - peer_state_lock.register_request(request_id.clone(), request).map_err(|e| { + let peer_state_lock = inner_state_lock.lock().unwrap(); + + let order = peer_state_lock.get_order(¶ms.order_id).map_err(|e| { + let response = LSPS1Response::GetOrderError(LSPSResponseError { + code: LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, + message: format!("Order with the requested order_id has not been found."), + data: None, + }); + let msg = LSPS1Message::Response(request_id.clone(), response).into(); + message_queue_notifier.enqueue(counterparty_node_id, msg); let err = format!("Failed to handle request due to: {}", e); let action = ErrorAction::IgnoreAndLog(Level::Error); LightningError { err, action } })?; - event_queue_notifier.enqueue(LSPS1ServiceEvent::CheckPaymentConfirmation { - request_id, - counterparty_node_id: *counterparty_node_id, + let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { order_id: params.order_id, + order: order.order_params.clone(), + order_state: order.order_state.clone(), + created_at: order.created_at.clone(), + payment: order.payment_details.clone(), + channel: order.channel_details.clone(), }); + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + Ok(()) }, None => { - return Err(LightningError { - err: format!("Received error response for a create order request from an unknown counterparty ({:?})", counterparty_node_id), - action: ErrorAction::IgnoreAndLog(Level::Info), + let response = LSPS1Response::GetOrderError(LSPSResponseError { + code: LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, + message: format!("Order with the requested order_id has not been found."), + data: None, }); + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(counterparty_node_id, msg); + Err(LightningError { + err: format!( + "Received get_order request from an unknown counterparty ({:?})", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) }, } - - Ok(()) } /// Used by LSP to give details to client regarding the status of channel opening. - /// Called to respond to client's `GetOrder` request. /// /// The LSP continously polls for checking payment confirmation on-chain or Lightning /// and then responds to client request. - /// - /// Should be called in response to receiving a [`LSPS1ServiceEvent::CheckPaymentConfirmation`] event. - /// - /// [`LSPS1ServiceEvent::CheckPaymentConfirmation`]: crate::lsps1::event::LSPS1ServiceEvent::CheckPaymentConfirmation pub fn update_order_status( - &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, order_state: LSPS1OrderState, channel_details: Option, ) -> Result<(), APIError> { - let mut message_queue_notifier = self.pending_messages.notifier(); - let outer_state_lock = self.per_peer_state.read().unwrap(); match outer_state_lock.get(&counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - let order = peer_state_lock - .update_order(&order_id, order_state, channel_details) - .map_err(|e| APIError::APIMisuseError { - err: format!("Failed to update order: {:?}", e), - })?; - - peer_state_lock.remove_request(&request_id).map_err(|e| { - debug_assert!(false, "Failed to send response due to: {}", e); - let err = format!("Failed to send response due to: {}", e); - APIError::APIMisuseError { err } - })?; + peer_state_lock.update_order(&order_id, order_state, channel_details).map_err( + |e| APIError::APIMisuseError { + err: format!("Failed to update order: {:?}", e), + }, + )?; - let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { - order_id, - order: order.order_params.clone(), - order_state: order.order_state.clone(), - created_at: order.created_at.clone(), - payment: order.payment_details.clone(), - channel: order.channel_details.clone(), - }); - let msg = LSPS1Message::Response(request_id, response).into(); - message_queue_notifier.enqueue(&counterparty_node_id, msg); Ok(()) }, None => Err(APIError::APIMisuseError { diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index e799cced976..ef210a34a16 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -10,7 +10,6 @@ use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps1::client::LSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; use lightning_liquidity::lsps1::event::LSPS1ServiceEvent; -use lightning_liquidity::lsps1::msgs::LSPS1OrderState; use lightning_liquidity::lsps1::msgs::{ LSPS1OnchainPaymentInfo, LSPS1Options, LSPS1OrderParams, LSPS1PaymentInfo, }; @@ -214,31 +213,6 @@ fn lsps1_happy_path() { .handle_custom_message(check_order_status, client_node_id) .unwrap(); - let _check_payment_confirmation_event = service_node.liquidity_manager.next_event().unwrap(); - - if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::CheckPaymentConfirmation { - request_id, - counterparty_node_id, - order_id, - }) = _check_payment_confirmation_event - { - assert_eq!(request_id, check_order_status_id); - assert_eq!(counterparty_node_id, client_node_id); - assert_eq!(order_id, expected_order_id.clone()); - } else { - panic!("Unexpected event"); - } - - let _ = service_handler - .update_order_status( - check_order_status_id.clone(), - client_node_id, - expected_order_id.clone(), - LSPS1OrderState::Created, - None, - ) - .unwrap(); - let order_status_response = get_lsps_message!(service_node, client_node_id); client_node From 5f43323f56efce69c49cf2cec83ca60d1dc4e166 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 11 Dec 2025 13:32:02 +0100 Subject: [PATCH 17/27] Add serialization logic for LSPS1 `PeerState` types We add the serializations for all types that will be persisted as part of the `PeerState`. --- lightning-liquidity/src/lsps1/msgs.rs | 81 ++++++++++++++++++++- lightning-liquidity/src/lsps1/peer_state.rs | 16 ++++ lightning/src/util/ser.rs | 52 +++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs index 4f79a13821a..5bf130400e1 100644 --- a/lightning-liquidity/src/lsps1/msgs.rs +++ b/lightning-liquidity/src/lsps1/msgs.rs @@ -19,8 +19,9 @@ use crate::lsps0::ser::{ }; use bitcoin::{Address, FeeRate, OutPoint}; - use lightning::offers::offer::Offer; +use lightning::util::ser::{Readable, Writeable}; +use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; @@ -39,6 +40,23 @@ pub(crate) const LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE: i32 = 101; #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct LSPS1OrderId(pub String); +impl Writeable for LSPS1OrderId { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + self.0.write(writer) + } +} + +impl Readable for LSPS1OrderId { + fn read( + reader: &mut R, + ) -> Result { + let inner = Readable::read(reader)?; + Ok(Self(inner)) + } +} + /// A request made to an LSP to retrieve the supported options. /// /// Please refer to the [bLIP-51 / LSPS1 @@ -128,6 +146,16 @@ pub struct LSPS1OrderParams { pub announce_channel: bool, } +impl_writeable_tlv_based!(LSPS1OrderParams, { + (0, lsp_balance_sat, required), + (2, client_balance_sat, required), + (4, required_channel_confirmations, required), + (6, funding_confirms_within_blocks, required), + (8, channel_expiry_blocks, required), + (10, token, option), + (12, announce_channel, required), +}); + /// A response to a [`LSPS1CreateOrderRequest`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1CreateOrderResponse { @@ -158,6 +186,12 @@ pub enum LSPS1OrderState { Failed, } +impl_writeable_tlv_based_enum!(LSPS1OrderState, + (0, Created) => {}, + (2, Completed) => {}, + (4, Failed) => {} +); + /// Details regarding how to pay for an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1PaymentInfo { @@ -169,6 +203,12 @@ pub struct LSPS1PaymentInfo { pub onchain: Option, } +impl_writeable_tlv_based!(LSPS1PaymentInfo, { + (0, bolt11, option), + (2, bolt12, option), + (4, onchain, option), +}); + /// A Lightning payment using BOLT 11. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1Bolt11PaymentInfo { @@ -186,6 +226,14 @@ pub struct LSPS1Bolt11PaymentInfo { pub invoice: Bolt11Invoice, } +impl_writeable_tlv_based!(LSPS1Bolt11PaymentInfo, { + (0, state, required), + (2, expires_at, required), + (4, fee_total_sat, required), + (6, order_total_sat, required), + (8, invoice, required), +}); + /// A Lightning payment using BOLT 12. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1Bolt12PaymentInfo { @@ -204,6 +252,14 @@ pub struct LSPS1Bolt12PaymentInfo { pub offer: Offer, } +impl_writeable_tlv_based!(LSPS1Bolt12PaymentInfo, { + (0, state, required), + (2, expires_at, required), + (4, fee_total_sat, required), + (6, order_total_sat, required), + (8, offer, required), +}); + /// An onchain payment. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1OnchainPaymentInfo { @@ -235,6 +291,17 @@ pub struct LSPS1OnchainPaymentInfo { pub refund_onchain_address: Option
        , } +impl_writeable_tlv_based!(LSPS1OnchainPaymentInfo, { + (0, state, required), + (2, expires_at, required), + (4, fee_total_sat, required), + (6, order_total_sat, required), + (8, address, required), + (10, min_onchain_payment_confirmations, option), + (12, min_fee_for_0conf, required), + (14, refund_onchain_address, option), +}); + /// The state of a payment. /// /// *Note*: Previously, the spec also knew a `CANCELLED` state for BOLT11 payments, which has since @@ -251,6 +318,12 @@ pub enum LSPS1PaymentState { Refunded, } +impl_writeable_tlv_based_enum!(LSPS1PaymentState, + (0, ExpectPayment) => {}, + (2, Paid) => {}, + (4, Refunded) => {} +); + /// Details regarding a detected on-chain payment. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1OnchainPayment { @@ -274,6 +347,12 @@ pub struct LSPS1ChannelInfo { pub expires_at: LSPSDateTime, } +impl_writeable_tlv_based!(LSPS1ChannelInfo, { + (0, funded_at, required), + (2, funding_outpoint, required), + (4, expires_at, required), +}); + /// A request made to an LSP to retrieve information about an previously made order. /// /// Please refer to the [bLIP-51 / LSPS1 diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 83b4b2f4d5b..796b4494a83 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -17,6 +17,9 @@ use super::msgs::{ use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId}; use crate::prelude::HashMap; +use lightning::impl_writeable_tlv_based; +use lightning::util::hash_tables::new_hash_map; + use core::fmt; #[derive(Default)] @@ -87,6 +90,11 @@ impl PeerState { } } +impl_writeable_tlv_based!(PeerState, { + (0, outbound_channels_by_order_id, required), + (_unused, pending_requests, (static_value, new_hash_map())), +}); + #[derive(Debug, Copy, Clone)] pub(super) enum PeerStateError { UnknownRequestId, @@ -112,3 +120,11 @@ pub(super) struct ChannelOrder { pub(super) payment_details: LSPS1PaymentInfo, pub(super) channel_details: Option, } + +impl_writeable_tlv_based!(ChannelOrder, { + (0, order_params, required), + (2, order_state, required), + (4, created_at, required), + (6, payment_details, required), + (8, channel_details, option), +}); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index f821aa5afc0..aad272526ef 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -22,10 +22,12 @@ use crate::sync::{Mutex, RwLock}; use core::cmp; use core::hash::Hash; use core::ops::Deref; +use core::str::FromStr; use alloc::collections::BTreeMap; use bitcoin::absolute::LockTime as AbsoluteLockTime; +use bitcoin::address::Address; use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::Encodable; use bitcoin::constants::ChainHash; @@ -41,10 +43,13 @@ use bitcoin::secp256k1::ecdsa; use bitcoin::secp256k1::schnorr; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{OutPoint, Transaction, TxOut}; +use bitcoin::FeeRate; use bitcoin::{consensus, Sequence, TxIn, Weight, Witness}; use dnssec_prover::rr::Name; +use lightning_invoice::Bolt11Invoice; + use crate::chain::ClaimId; #[cfg(taproot)] use crate::ln::msgs::PartialSignatureWithNonce; @@ -1477,6 +1482,53 @@ impl Readable for OutPoint { } } +impl Writeable for Address { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_string().write(w)?; + Ok(()) + } +} + +impl Readable for Address { + fn read(r: &mut R) -> Result { + let addr_string: String = Readable::read(r)?; + let addr = Address::from_str(&addr_string) + .map_err(|_| DecodeError::InvalidValue)? + .assume_checked(); + Ok(addr) + } +} + +impl Writeable for FeeRate { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_sat_per_kwu().write(w)?; + Ok(()) + } +} + +impl Readable for FeeRate { + fn read(r: &mut R) -> Result { + let sat_per_kwu: u64 = Readable::read(r)?; + Ok(FeeRate::from_sat_per_kwu(sat_per_kwu)) + } +} + +impl Writeable for Bolt11Invoice { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_string().write(w)?; + Ok(()) + } +} + +impl Readable for Bolt11Invoice { + fn read(r: &mut R) -> Result { + let invoice_string: String = Readable::read(r)?; + let invoice = + Bolt11Invoice::from_str(&invoice_string).map_err(|_| DecodeError::InvalidValue)?; + Ok(invoice) + } +} + macro_rules! impl_consensus_ser { ($bitcoin_type: ty) => { impl Writeable for $bitcoin_type { From 2cc39e5464ae829717a66b1565362737c13af92b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 11 Dec 2025 14:24:04 +0100 Subject: [PATCH 18/27] Implement `LSPS1ServiceHandler` persistence and state pruning We follow the model already employed in LSPS2/LSPS5 and implement state pruning and persistence for `LSPS1ServiceHandler` state. --- lightning-liquidity/src/lsps1/peer_state.rs | 55 +++ lightning-liquidity/src/lsps1/service.rs | 312 ++++++++++++++++-- lightning-liquidity/src/manager.rs | 21 +- lightning-liquidity/src/persist.rs | 5 + .../tests/lsps1_integration_tests.rs | 2 +- 5 files changed, 368 insertions(+), 27 deletions(-) diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index 796b4494a83..f9e7635d78c 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -26,6 +26,7 @@ use core::fmt; pub(super) struct PeerState { outbound_channels_by_order_id: HashMap, pending_requests: HashMap, + needs_persist: bool, } impl PeerState { @@ -43,6 +44,7 @@ impl PeerState { channel_details, }; self.outbound_channels_by_order_id.insert(order_id, channel_order.clone()); + self.needs_persist |= true; channel_order } @@ -66,6 +68,7 @@ impl PeerState { .ok_or(PeerStateError::UnknownOrderId)?; order.order_state = order_state; order.channel_details = channel_details; + self.needs_persist |= true; Ok(()) } @@ -88,11 +91,39 @@ impl PeerState { pub(super) fn has_active_requests(&self) -> bool { !self.outbound_channels_by_order_id.is_empty() } + + pub(super) fn needs_persist(&self) -> bool { + self.needs_persist + } + + pub(super) fn set_needs_persist(&mut self, needs_persist: bool) { + self.needs_persist = needs_persist; + } + + pub(super) fn is_prunable(&self) -> bool { + // Return whether the entire state is empty. + self.pending_requests.is_empty() && self.outbound_channels_by_order_id.is_empty() + } + + pub(super) fn prune_pending_requests(&mut self) { + self.pending_requests.clear() + } + + pub(super) fn prune_expired_request_state(&mut self) { + self.outbound_channels_by_order_id.retain(|_order_id, entry| { + if entry.is_prunable() { + self.needs_persist |= true; + return false; + } + true + }); + } } impl_writeable_tlv_based!(PeerState, { (0, outbound_channels_by_order_id, required), (_unused, pending_requests, (static_value, new_hash_map())), + (_unused, needs_persist, (static_value, false)), }); #[derive(Debug, Copy, Clone)] @@ -121,6 +152,30 @@ pub(super) struct ChannelOrder { pub(super) channel_details: Option, } +impl ChannelOrder { + fn is_prunable(&self) -> bool { + let all_payment_details_expired; + #[cfg(feature = "time")] + { + let details = &self.payment_details; + all_payment_details_expired = + details.bolt11.as_ref().map_or(true, |d| d.expires_at.is_past()) + && details.bolt12.as_ref().map_or(true, |d| d.expires_at.is_past()) + && details.onchain.as_ref().map_or(true, |d| d.expires_at.is_past()); + } + #[cfg(not(feature = "time"))] + { + // TODO: We need to find a way to check expiry times in no-std builds. + all_payment_details_expired = false; + } + + let created_or_failed = + matches!(self.order_state, LSPS1OrderState::Created | LSPS1OrderState::Failed); + + all_payment_details_expired && created_or_failed + } +} + impl_writeable_tlv_based!(ChannelOrder, { (0, order_params, required), (2, order_state, required), diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 218feefe5c0..04ec61fdb66 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -9,9 +9,14 @@ //! Contains the main bLIP-51 / LSPS1 server object, [`LSPS1ServiceHandler`]. -use alloc::string::String; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::future::Future as StdFuture; use core::ops::Deref; +use core::pin::pin; +use core::sync::atomic::{AtomicUsize, Ordering}; +use core::task; use super::event::LSPS1ServiceEvent; use super::msgs::{ @@ -28,9 +33,14 @@ use crate::events::EventQueue; use crate::lsps0::ser::{ LSPSDateTime, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError, }; +use crate::persist::{ + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use crate::prelude::hash_map::Entry; use crate::prelude::{new_hash_map, HashMap}; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils; +use crate::utils::async_poll::dummy_waker; use crate::utils::time::TimeProvider; use lightning::ln::channelmanager::AChannelManager; @@ -39,6 +49,7 @@ use lightning::sign::EntropySource; use lightning::util::errors::APIError; use lightning::util::logger::Level; use lightning::util::persist::KVStore; +use lightning::util::ser::Writeable; use bitcoin::secp256k1::PublicKey; @@ -61,9 +72,11 @@ where { entropy_source: ES, _channel_manager: CM, + kv_store: K, pending_messages: Arc, pending_events: Arc>, per_peer_state: RwLock>>, + persistence_in_flight: AtomicUsize, time_provider: TP, config: LSPS1ServiceConfig, } @@ -79,15 +92,17 @@ where /// Constructs a `LSPS1ServiceHandler`. pub(crate) fn new( entropy_source: ES, pending_messages: Arc, - pending_events: Arc>, channel_manager: CM, time_provider: TP, + pending_events: Arc>, channel_manager: CM, kv_store: K, time_provider: TP, config: LSPS1ServiceConfig, ) -> Self { Self { entropy_source, _channel_manager: channel_manager, + kv_store, pending_messages, pending_events, per_peer_state: RwLock::new(new_hash_map()), + persistence_in_flight: AtomicUsize::new(0), time_provider, config, } @@ -106,12 +121,153 @@ where /// Pending requests that are still awaiting our response are deliberately NOT counted. pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool { let outer_state_lock = self.per_peer_state.read().unwrap(); - outer_state_lock.get(counterparty_node_id).map_or(false, |inner| { + outer_state_lock.get(counterparty_node_id).is_some_and(|inner| { let peer_state = inner.lock().unwrap(); peer_state.has_active_requests() }) } + pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { + let outer_state_lock = self.per_peer_state.write().unwrap(); + if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + // We clean up the peer state, but leave removing the peer entry to the prune logic in + // `persist` which removes it from the store. + peer_state_lock.prune_pending_requests(); + peer_state_lock.prune_expired_request_state(); + } + } + + pub(crate) async fn persist(&self) -> Result { + // TODO: We should eventually persist in parallel, however, when we do, we probably want to + // introduce some batching to upper-bound the number of requests inflight at any given + // time. + let mut did_persist = false; + + if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 { + // If we're not the first event processor to get here, just return early, the increment + // we just did will be treated as "go around again" at the end. + return Ok(did_persist); + } + + loop { + let mut need_remove = Vec::new(); + let mut need_persist = Vec::new(); + + { + // First build a list of peers to persist and prune with the read lock. This allows + // us to avoid the write lock unless we actually need to remove a node. + let outer_state_lock = self.per_peer_state.read().unwrap(); + for (counterparty_node_id, inner_state_lock) in outer_state_lock.iter() { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.prune_expired_request_state(); + let is_prunable = peer_state_lock.is_prunable(); + if is_prunable { + need_remove.push(*counterparty_node_id); + } else if peer_state_lock.needs_persist() { + need_persist.push(*counterparty_node_id); + } + } + } + + for counterparty_node_id in need_persist.into_iter() { + debug_assert!(!need_remove.contains(&counterparty_node_id)); + self.persist_peer_state(counterparty_node_id).await?; + did_persist = true; + } + + for counterparty_node_id in need_remove { + let mut future_opt = None; + { + // We need to take the `per_peer_state` write lock to remove an entry, but also + // have to hold it until after the `remove` call returns (but not through + // future completion) to ensure that writes for the peer's state are + // well-ordered with other `persist_peer_state` calls even across the removal + // itself. + let mut per_peer_state = self.per_peer_state.write().unwrap(); + if let Entry::Occupied(mut entry) = per_peer_state.entry(counterparty_node_id) { + let state = entry.get_mut().get_mut().unwrap(); + if state.is_prunable() { + entry.remove(); + let key = counterparty_node_id.to_string(); + future_opt = Some(self.kv_store.remove( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &key, + true, + )); + } else { + // If the peer got new state, force a re-persist of the current state. + state.set_needs_persist(true); + } + } else { + // This should never happen, we can only have one `persist` call + // in-progress at once and map entries are only removed by it. + debug_assert!(false); + } + } + if let Some(future) = future_opt { + future.await?; + did_persist = true; + } else { + self.persist_peer_state(counterparty_node_id).await?; + } + } + + if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 { + // If another thread incremented the state while we were running we should go + // around again, but only once. + self.persistence_in_flight.store(1, Ordering::Release); + continue; + } + break; + } + + Ok(did_persist) + } + + async fn persist_peer_state( + &self, counterparty_node_id: PublicKey, + ) -> Result<(), lightning::io::Error> { + let fut = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + None => { + // We dropped the peer state by now. + return Ok(()); + }, + Some(entry) => { + let mut peer_state_lock = entry.lock().unwrap(); + if !peer_state_lock.needs_persist() { + // We already have persisted otherwise by now. + return Ok(()); + } else { + peer_state_lock.set_needs_persist(false); + let key = counterparty_node_id.to_string(); + let encoded = peer_state_lock.encode(); + // Begin the write with the entry lock held. This avoids racing with + // potentially-in-flight `persist` calls writing state for the same peer. + self.kv_store.write( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &key, + encoded, + ) + } + }, + } + }; + + fut.await.map_err(|e| { + self.per_peer_state + .read() + .unwrap() + .get(&counterparty_node_id) + .map(|p| p.lock().unwrap().set_needs_persist(true)); + e + }) + } + fn handle_get_info_request( &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { @@ -180,14 +336,14 @@ where /// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] event. /// /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails - pub fn send_payment_details( - &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, + pub async fn send_payment_details( + &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, payment_details: LSPS1PaymentInfo, ) -> Result<(), APIError> { let mut message_queue_notifier = self.pending_messages.notifier(); + let mut should_persist = false; - let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(counterparty_node_id) { + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); let request = peer_state_lock.remove_request(&request_id).map_err(|e| { @@ -208,6 +364,7 @@ where created_at, payment_details, ); + should_persist |= peer_state_lock.needs_persist(); let response = LSPS1Response::CreateOrder(LSPS1CreateOrderResponse { order: order.order_params, @@ -219,8 +376,7 @@ where channel: order.channel_details, }); let msg = LSPS1Message::Response(request_id, response).into(); - message_queue_notifier.enqueue(counterparty_node_id, msg); - Ok(()) + message_queue_notifier.enqueue(&counterparty_node_id, msg); }, t => { debug_assert!( @@ -236,10 +392,25 @@ where }, } }, - None => Err(APIError::APIMisuseError { - err: format!("No state for the counterparty exists: {}", counterparty_node_id), - }), + None => { + return Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {}", counterparty_node_id), + }); + }, + } + + if should_persist { + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state for {}: {}", + counterparty_node_id, e + ), + } + })?; } + + Ok(()) } fn handle_get_order_request( @@ -300,13 +471,12 @@ where /// /// The LSP continously polls for checking payment confirmation on-chain or Lightning /// and then responds to client request. - pub fn update_order_status( + pub async fn update_order_status( &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, order_state: LSPS1OrderState, channel_details: Option, ) -> Result<(), APIError> { - let outer_state_lock = self.per_peer_state.read().unwrap(); - - match outer_state_lock.get(&counterparty_node_id) { + let mut should_persist = false; + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); peer_state_lock.update_order(&order_id, order_state, channel_details).map_err( @@ -314,13 +484,27 @@ where err: format!("Failed to update order: {:?}", e), }, )?; - - Ok(()) + should_persist |= peer_state_lock.needs_persist(); }, - None => Err(APIError::APIMisuseError { - err: format!("No existing state with counterparty {}", counterparty_node_id), - }), + None => { + return Err(APIError::APIMisuseError { + err: format!("No existing state with counterparty {}", counterparty_node_id), + }); + }, + } + + if should_persist { + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state for {}: {}", + counterparty_node_id, e + ), + } + })?; } + + Ok(()) } fn generate_order_id(&self) -> LSPS1OrderId { @@ -329,6 +513,92 @@ where } } +/// A synchroneous wrapper around [`LSPS1ServiceHandler`] to be used in contexts where async is not +/// available. +pub struct LSPS1ServiceHandlerSync< + 'a, + ES: Deref, + CM: Deref + Clone, + K: Deref + Clone, + TP: Deref + Clone, +> where + ES::Target: EntropySource, + CM::Target: AChannelManager, + K::Target: KVStore, + TP::Target: TimeProvider, +{ + inner: &'a LSPS1ServiceHandler, +} + +impl<'a, ES: Deref, CM: Deref + Clone, K: Deref + Clone, TP: Deref + Clone> + LSPS1ServiceHandlerSync<'a, ES, CM, K, TP> +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + K::Target: KVStore, + TP::Target: TimeProvider, +{ + pub(crate) fn from_inner(inner: &'a LSPS1ServiceHandler) -> Self { + Self { inner } + } + + /// Returns a reference to the used config. + /// + /// Wraps [`LSPS1ServiceHandler::config`]. + pub fn config(&self) -> &LSPS1ServiceConfig { + &self.inner.config + } + + /// Used by LSP to send response containing details regarding the channel fees and payment information. + /// + /// Wraps [`LSPS1ServiceHandler::send_payment_details`]. + pub fn send_payment_details( + &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, + payment_details: LSPS1PaymentInfo, + ) -> Result<(), APIError> { + let mut fut = pin!(self.inner.send_payment_details( + request_id, + counterparty_node_id, + payment_details + )); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } + + /// Used by LSP to give details to client regarding the status of channel opening. + /// + /// Wraps [`LSPS1ServiceHandler::update_order_status`]. + pub fn update_order_status( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + order_state: LSPS1OrderState, channel_details: Option, + ) -> Result<(), APIError> { + let mut fut = pin!(self.inner.update_order_status( + counterparty_node_id, + order_id, + order_state, + channel_details + )); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } +} + impl LSPSProtocolMessageHandler for LSPS1ServiceHandler where diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 7075a10f845..43361273cd3 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -30,7 +30,7 @@ use crate::persist::{ use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler}; use crate::lsps1::msgs::LSPS1Message; #[cfg(lsps1_service)] -use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler}; +use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler, LSPS1ServiceHandlerSync}; use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; use crate::lsps2::msgs::LSPS2Message; @@ -487,6 +487,7 @@ where Arc::clone(&pending_messages), Arc::clone(&pending_events), channel_manager.clone(), + kv_store.clone(), time_provider, config.clone(), ) @@ -648,6 +649,11 @@ where let mut did_persist = false; did_persist |= self.pending_events.persist().await?; + #[cfg(lsps1_service)] + if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() { + did_persist |= lsps1_service_handler.persist().await?; + } + if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { did_persist |= lsps2_service_handler.persist().await?; } @@ -912,6 +918,11 @@ where // If the peer was misbehaving, drop it from the ignored list to cleanup the kept state. self.ignored_peers.write().unwrap().remove(&counterparty_node_id); + #[cfg(lsps1_service)] + if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() { + lsps1_service_handler.peer_disconnected(counterparty_node_id); + } + if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { lsps2_service_handler.peer_disconnected(counterparty_node_id); } @@ -1073,10 +1084,10 @@ where /// /// Wraps [`LiquidityManager::lsps1_service_handler`]. #[cfg(lsps1_service)] - pub fn lsps1_service_handler( - &self, - ) -> Option<&LSPS1ServiceHandler, TP>> { - self.inner.lsps1_service_handler() + pub fn lsps1_service_handler<'a>( + &'a self, + ) -> Option, TP>> { + self.inner.lsps1_service_handler.as_ref().map(|r| LSPS1ServiceHandlerSync::from_inner(r)) } /// Returns a reference to the LSPS2 client-side handler. diff --git a/lightning-liquidity/src/persist.rs b/lightning-liquidity/src/persist.rs index ec0d5a6ddd3..dda4b12d857 100644 --- a/lightning-liquidity/src/persist.rs +++ b/lightning-liquidity/src/persist.rs @@ -41,6 +41,11 @@ pub const LIQUIDITY_MANAGER_EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE: &str = /// [`LiquidityManager`]: crate::LiquidityManager pub const LIQUIDITY_MANAGER_EVENT_QUEUE_PERSISTENCE_KEY: &str = "event_queue"; +/// The secondary namespace under which the [`LSPS1ServiceHandler`] data will be persisted. +/// +/// [`LSPS1ServiceHandler`]: crate::lsps1::service::LSPS1ServiceHandler +pub const LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE: &str = "lsps1_service"; + /// The secondary namespace under which the [`LSPS2ServiceHandler`] data will be persisted. /// /// [`LSPS2ServiceHandler`]: crate::lsps2::service::LSPS2ServiceHandler diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index ef210a34a16..0343116a650 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -174,7 +174,7 @@ fn lsps1_happy_path() { serde_json::from_str(json_str).expect("Failed to parse JSON"); let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; service_handler - .send_payment_details(_create_order_id.clone(), &client_node_id, payment_info.clone()) + .send_payment_details(_create_order_id.clone(), client_node_id, payment_info.clone()) .unwrap(); let create_order_response = get_lsps_message!(service_node, client_node_id); From 988a19a6a0bfd165e8adfcea678130fb70e44495 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 16:17:31 +0100 Subject: [PATCH 19/27] Read persisted LSPS1ServiceHandler state on startup .. we read the persisted state in `LiquidityManager::new` --- lightning-liquidity/src/lsps1/mod.rs | 2 +- lightning-liquidity/src/lsps1/peer_state.rs | 2 +- lightning-liquidity/src/lsps1/service.rs | 10 ++--- lightning-liquidity/src/manager.rs | 34 +++++++++------ lightning-liquidity/src/persist.rs | 47 +++++++++++++++++++++ 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/lightning-liquidity/src/lsps1/mod.rs b/lightning-liquidity/src/lsps1/mod.rs index bdfc4045f54..2270abe2fa3 100644 --- a/lightning-liquidity/src/lsps1/mod.rs +++ b/lightning-liquidity/src/lsps1/mod.rs @@ -13,6 +13,6 @@ pub mod client; pub mod event; pub mod msgs; #[cfg(lsps1_service)] -mod peer_state; +pub(crate) mod peer_state; #[cfg(lsps1_service)] pub mod service; diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs index f9e7635d78c..e0377dfc2db 100644 --- a/lightning-liquidity/src/lsps1/peer_state.rs +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -23,7 +23,7 @@ use lightning::util::hash_tables::new_hash_map; use core::fmt; #[derive(Default)] -pub(super) struct PeerState { +pub(crate) struct PeerState { outbound_channels_by_order_id: HashMap, pending_requests: HashMap, needs_persist: bool, diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 04ec61fdb66..a1096245739 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -37,7 +37,7 @@ use crate::persist::{ LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::prelude::hash_map::Entry; -use crate::prelude::{new_hash_map, HashMap}; +use crate::prelude::HashMap; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils; use crate::utils::async_poll::dummy_waker; @@ -91,9 +91,9 @@ where { /// Constructs a `LSPS1ServiceHandler`. pub(crate) fn new( - entropy_source: ES, pending_messages: Arc, - pending_events: Arc>, channel_manager: CM, kv_store: K, time_provider: TP, - config: LSPS1ServiceConfig, + per_peer_state: HashMap>, entropy_source: ES, + pending_messages: Arc, pending_events: Arc>, + channel_manager: CM, kv_store: K, time_provider: TP, config: LSPS1ServiceConfig, ) -> Self { Self { entropy_source, @@ -101,7 +101,7 @@ where kv_store, pending_messages, pending_events, - per_peer_state: RwLock::new(new_hash_map()), + per_peer_state: RwLock::new(per_peer_state), persistence_in_flight: AtomicUsize::new(0), time_provider, config, diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 43361273cd3..96aa8055853 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -23,6 +23,8 @@ use crate::lsps5::client::{LSPS5ClientConfig, LSPS5ClientHandler}; use crate::lsps5::msgs::LSPS5Message; use crate::lsps5::service::{LSPS5ServiceConfig, LSPS5ServiceHandler}; use crate::message_queue::MessageQueue; +#[cfg(lsps1_service)] +use crate::persist::read_lsps1_service_peer_states; use crate::persist::{ read_event_queue, read_lsps2_service_peer_states, read_lsps5_service_peer_states, }; @@ -475,24 +477,32 @@ where }); #[cfg(lsps1_service)] - let lsps1_service_handler = service_config.as_ref().and_then(|config| { - if let Some(number) = - as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER - { - supported_protocols.push(number); - } - config.lsps1_service_config.as_ref().map(|config| { - LSPS1ServiceHandler::new( + let lsps1_service_handler = if let Some(service_config) = service_config.as_ref() { + if let Some(lsps1_service_config) = service_config.lsps1_service_config.as_ref() { + if let Some(number) = + as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER + { + supported_protocols.push(number); + } + + let peer_states = read_lsps1_service_peer_states(kv_store.clone()).await?; + + Some(LSPS1ServiceHandler::new( + peer_states, entropy_source.clone(), Arc::clone(&pending_messages), Arc::clone(&pending_events), channel_manager.clone(), kv_store.clone(), time_provider, - config.clone(), - ) - }) - }); + lsps1_service_config.clone(), + )) + } else { + None + } + } else { + None + }; let lsps0_client_handler = LSPS0ClientHandler::new( entropy_source.clone(), diff --git a/lightning-liquidity/src/persist.rs b/lightning-liquidity/src/persist.rs index dda4b12d857..48ecc3b18dd 100644 --- a/lightning-liquidity/src/persist.rs +++ b/lightning-liquidity/src/persist.rs @@ -10,6 +10,8 @@ //! Types and utils for persistence. use crate::events::{EventQueueDeserWrapper, LiquidityEvent}; +#[cfg(lsps1_service)] +use crate::lsps1::peer_state::PeerState as LSPS1ServicePeerState; use crate::lsps2::service::PeerState as LSPS2ServicePeerState; use crate::lsps5::service::PeerState as LSPS5ServicePeerState; use crate::prelude::{new_hash_map, HashMap}; @@ -90,6 +92,51 @@ where Ok(Some(queue.0)) } +#[cfg(lsps1_service)] +pub(crate) async fn read_lsps1_service_peer_states( + kv_store: K, +) -> Result>, lightning::io::Error> +where + K::Target: KVStore, +{ + let mut res = new_hash_map(); + + for stored_key in kv_store + .list( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + ) + .await? + { + let mut reader = Cursor::new( + kv_store + .read( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &stored_key, + ) + .await?, + ); + + let peer_state = LSPS1ServicePeerState::read(&mut reader).map_err(|_| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + "Failed to deserialize LSPS1 peer state", + ) + })?; + + let key = PublicKey::from_str(&stored_key).map_err(|_| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + "Failed to deserialize stored key entry", + ) + })?; + + res.insert(key, Mutex::new(peer_state)); + } + Ok(res) +} + pub(crate) async fn read_lsps2_service_peer_states( kv_store: K, ) -> Result>, lightning::io::Error> From f320efbfa08deef252308ce9fef4361fee001d71 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 16:01:30 +0100 Subject: [PATCH 20/27] Add test case asserting `LSPS1ServiceState` is persisted across restarts Co-authored by Claude AI --- .../tests/lsps1_integration_tests.rs | 259 +++++++++++++++++- 1 file changed, 257 insertions(+), 2 deletions(-) diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index 0343116a650..01c9a38b982 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -15,16 +15,22 @@ use lightning_liquidity::lsps1::msgs::{ }; use lightning_liquidity::lsps1::service::LSPS1ServiceConfig; use lightning_liquidity::utils::time::DefaultTimeProvider; -use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; +use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig}; use lightning::ln::functional_test_utils::{ create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs, }; -use lightning::util::test_utils::TestStore; +use lightning::util::test_utils::{TestBroadcaster, TestStore}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Address, Network}; + +use std::str::FromStr; use std::sync::Arc; use lightning::ln::functional_test_utils::{create_network, Node}; +use lightning_liquidity::lsps1::msgs::LSPS1OrderId; +use lightning_liquidity::utils::time::TimeProvider; fn build_lsps1_configs( supported_options: LSPS1Options, @@ -240,3 +246,252 @@ fn lsps1_happy_path() { panic!("Unexpected event"); } } + +#[test] +fn lsps1_service_handler_persistence_across_restarts() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Create shared KV store for service node that will persist across restarts + let service_kv_store = Arc::new(TestStore::new(false)); + let client_kv_store = Arc::new(TestStore::new(false)); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let service_config = LiquidityServiceConfig { + lsps1_service_config: Some(LSPS1ServiceConfig { + supported_options: supported_options.clone(), + token: None, + }), + lsps2_service_config: None, + lsps5_service_config: None, + advertise_service: true, + }; + let time_provider: Arc = Arc::new(DefaultTimeProvider); + + // Variables to carry state between scopes + let client_node_id: PublicKey; + let expected_order_id: LSPS1OrderId; + let order_params: LSPS1OrderParams; + let payment_info: LSPS1PaymentInfo; + + // First scope: Setup, persistence, and dropping of all node objects + { + let LSPSNodes { service_node, client_node } = setup_test_lsps1_nodes_with_kv_stores( + nodes, + Arc::clone(&service_kv_store), + client_kv_store, + supported_options.clone(), + ); + + let service_node_id = service_node.inner.node.get_our_node_id(); + client_node_id = client_node.inner.node.get_our_node_id(); + + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Request supported options + let _request_supported_options_id = + client_handler.request_supported_options(service_node_id); + let request_supported_options = get_lsps_message!(client_node, service_node_id); + + service_node + .liquidity_manager + .handle_custom_message(request_supported_options, client_node_id) + .unwrap(); + + let get_info_message = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(get_info_message, service_node_id) + .unwrap(); + + let _get_info_event = client_node.liquidity_manager.next_event().unwrap(); + + // Create an order to establish persistent state + order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + .. + }) = request_for_payment_event + { + request_id + } else { + panic!("Unexpected event"); + }; + + // Service sends payment details, creating persistent order state + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2035-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + service_handler + .send_payment_details(request_id.clone(), client_node_id, payment_info.clone()) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + expected_order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + order_id, + .. + }) = order_created_event + { + assert_eq!(request_id, create_order_id); + order_id + } else { + panic!("Unexpected event"); + }; + + // Trigger persistence by calling persist + service_node.liquidity_manager.persist().unwrap(); + + // All node objects are dropped at the end of this scope + } + + // Second scope: Recovery from persisted store and verification + { + // Create fresh node configurations for restart + let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); + + // Create a new LiquidityManager with the same configuration and KV store to simulate restart + let service_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let client_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let client_kv_store_restart = Arc::new(TestStore::new(false)); + + let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes_restart[0].keys_manager, + nodes_restart[0].keys_manager, + nodes_restart[0].node, + service_kv_store, + service_transaction_broadcaster, + Some(service_config), + None, + Arc::clone(&time_provider), + ) + .unwrap(); + + // Create a fresh client to query the restarted service + let lsps1_client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let client_config = LiquidityClientConfig { + lsps1_client_config: Some(lsps1_client_config), + lsps2_client_config: None, + lsps5_client_config: None, + }; + + let client_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes_restart[1].keys_manager, + nodes_restart[1].keys_manager, + nodes_restart[1].node, + client_kv_store_restart, + client_transaction_broadcaster, + None, + Some(client_config), + time_provider, + ) + .unwrap(); + + let service_node_id = nodes_restart[0].node.get_our_node_id(); + let client_node_id_restart = nodes_restart[1].node.get_our_node_id(); + + // Verify node IDs match (since we use same node_cfgs) + assert_eq!(client_node_id_restart, client_node_id); + + // Use the client to send a GetOrder request + let client_handler = client_lm.lsps1_client_handler().unwrap(); + let check_order_status_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + + // Get the request message from client + let pending_client_msgs = client_lm.get_and_clear_pending_msg(); + assert_eq!(pending_client_msgs.len(), 1); + let (target_node_id, request_msg) = pending_client_msgs.into_iter().next().unwrap(); + assert_eq!(target_node_id, service_node_id); + + // Pass the request to the restarted service + restarted_service_lm.handle_custom_message(request_msg, client_node_id).unwrap(); + + // Get the response from the service + let pending_service_msgs = restarted_service_lm.get_and_clear_pending_msg(); + assert_eq!(pending_service_msgs.len(), 1); + let (target_node_id, response_msg) = pending_service_msgs.into_iter().next().unwrap(); + assert_eq!(target_node_id, client_node_id); + + // Pass the response to the client + client_lm.handle_custom_message(response_msg, service_node_id).unwrap(); + + // Verify the client receives the order status event with correct data + let order_status_event = client_lm.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) = order_status_event + { + assert_eq!(request_id, check_order_status_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(order_id, expected_order_id); + assert_eq!(order, order_params); + assert_eq!(payment, payment_info); + assert!(channel.is_none()); + } else { + panic!("Expected OrderStatus event after restart, got: {:?}", order_status_event); + } + } +} From f509a45f904bcfa472839424e0c2a7bdbbefbbfc Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 13:23:43 +0100 Subject: [PATCH 21/27] Add some checks on provided payment details As per spec, we check that the user provides at least one payment detail *and* that they don't provide onchain payment details if `refund_onchain_address` is unset. --- lightning-liquidity/src/lsps1/service.rs | 17 +++++++++++++++++ .../tests/lsps1_integration_tests.rs | 11 +++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index a1096245739..5530b85d50c 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -358,6 +358,23 @@ where let created_at = LSPSDateTime::new_from_duration_since_epoch( self.time_provider.duration_since_epoch(), ); + + if payment_details.bolt11.is_none() + && payment_details.bolt12.is_none() + && payment_details.onchain.is_none() + { + let err = "At least one payment option must be provided".to_string(); + return Err(APIError::APIMisuseError { err }); + } + + if params.refund_onchain_address.is_none() + && payment_details.onchain.is_some() + { + // bLIP-51: 'LSP MUST disable on-chain payments if the client omits this field.' + let err = "Onchain payments must be disabled if no refund_onchain_address is set.".to_string(); + return Err(APIError::APIMisuseError { err }); + } + let order = peer_state_lock.new_order( order_id.clone(), params.order, diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index 01c9a38b982..b87568d8edf 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -145,8 +145,15 @@ fn lsps1_happy_path() { announce_channel: true, }; - let _create_order_id = - client_handler.create_order(&service_node_id, order_params.clone(), None); + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let _create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); let create_order = get_lsps_message!(client_node, service_node_id); service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); From b1ef5154a7645ffbb4deb84e7218dc08ffd3a26c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 14:37:41 +0100 Subject: [PATCH 22/27] Don't hold write lock in `LSPS{1,2}ServiceHandler::peer_disconnected` .. as there's no need to do so. --- lightning-liquidity/src/lsps1/service.rs | 2 +- lightning-liquidity/src/lsps2/service.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 5530b85d50c..64ef5771b2b 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -128,7 +128,7 @@ where } pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { - let outer_state_lock = self.per_peer_state.write().unwrap(); + let outer_state_lock = self.per_peer_state.read().unwrap(); if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { let mut peer_state_lock = inner_state_lock.lock().unwrap(); // We clean up the peer state, but leave removing the peer entry to the prune logic in diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 1b5bf964996..25684e9127f 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -1875,7 +1875,7 @@ where } pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { - let outer_state_lock = self.per_peer_state.write().unwrap(); + let outer_state_lock = self.per_peer_state.read().unwrap(); if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { let mut peer_state_lock = inner_state_lock.lock().unwrap(); // We clean up the peer state, but leave removing the peer entry to the prune logic in From 04ba24b696e946a4c3c235f56f0331814cbade84 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 15:01:05 +0100 Subject: [PATCH 23/27] Add `invalid_token_provided` API method We add a method that allows the LSP to signal to the client the token they used was invalid. We use the `102` error code as proposed in https://github.com/lightning/blips/pull/68. --- lightning-liquidity/src/lsps1/msgs.rs | 1 + lightning-liquidity/src/lsps1/service.rs | 50 +++++++++++++++++-- .../tests/lsps0_integration_tests.rs | 2 +- .../tests/lsps1_integration_tests.rs | 3 +- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs index 5bf130400e1..a2382e0b71c 100644 --- a/lightning-liquidity/src/lsps1/msgs.rs +++ b/lightning-liquidity/src/lsps1/msgs.rs @@ -35,6 +35,7 @@ pub(crate) const _LSPS1_CREATE_ORDER_REQUEST_INVALID_PARAMS_ERROR_CODE: i32 = -3 pub(crate) const LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE: i32 = 100; #[cfg(lsps1_service)] pub(crate) const LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE: i32 = 101; +pub(crate) const LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE: i32 = 102; /// The identifier of an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 64ef5771b2b..0294d216182 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -9,7 +9,7 @@ //! Contains the main bLIP-51 / LSPS1 server object, [`LSPS1ServiceHandler`]. -use alloc::string::{String, ToString}; +use alloc::string::ToString; use alloc::vec::Vec; use core::future::Future as StdFuture; @@ -24,6 +24,7 @@ use super::msgs::{ LSPS1GetOrderRequest, LSPS1Message, LSPS1Options, LSPS1OrderId, LSPS1OrderParams, LSPS1OrderState, LSPS1PaymentInfo, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, + LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, }; use super::peer_state::PeerState; @@ -56,8 +57,6 @@ use bitcoin::secp256k1::PublicKey; /// Server-side configuration options for bLIP-51 / LSPS1 channel requests. #[derive(Clone, Debug)] pub struct LSPS1ServiceConfig { - /// A token to be send with each channel request. - pub token: Option, /// The options supported by the LSP. pub supported_options: LSPS1Options, } @@ -430,6 +429,42 @@ where Ok(()) } + /// Used by LSP to inform a client that an order was rejected because the used token was invalid. + /// + /// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] + /// event if the provided token is invalid. + /// + /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails + pub fn invalid_token_provided( + &self, counterparty_node_id: PublicKey, request_id: LSPSRequestId, + ) -> Result<(), APIError> { + let mut message_queue_notifier = self.pending_messages.notifier(); + + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.remove_request(&request_id).map_err(|e| { + debug_assert!(false, "Failed to send response due to: {}", e); + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + + let response = LSPS1Response::CreateOrderError(LSPSResponseError { + code: LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, + message: "An unrecognized or stale token was provided".to_string(), + data: None, + }); + + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + Ok(()) + }, + None => Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {}", counterparty_node_id), + }), + } + } + fn handle_get_order_request( &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, params: LSPS1GetOrderRequest, @@ -590,6 +625,15 @@ where } } + /// Used by LSP to inform a client that an order was rejected because the used token was invalid. + /// + /// Wraps [`LSPS1ServiceHandler::invalid_token_provided`]. + pub fn invalid_token_provided( + &self, counterparty_node_id: PublicKey, request_id: LSPSRequestId, + ) -> Result<(), APIError> { + self.inner.invalid_token_provided(counterparty_node_id, request_id) + } + /// Used by LSP to give details to client regarding the status of channel opening. /// /// Wraps [`LSPS1ServiceHandler::update_order_status`]. diff --git a/lightning-liquidity/tests/lsps0_integration_tests.rs b/lightning-liquidity/tests/lsps0_integration_tests.rs index 7f0e01bde92..58d9e867398 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -49,7 +49,7 @@ fn list_protocols_integration_test() { min_channel_balance_sat: 100_000, max_channel_balance_sat: 100_000_000, }; - LSPS1ServiceConfig { supported_options, token: None } + LSPS1ServiceConfig { supported_options } }; let lsps5_service_config = LSPS5ServiceConfig::default(); let service_config = LiquidityServiceConfig { diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index b87568d8edf..ebac780b939 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -35,7 +35,7 @@ use lightning_liquidity::utils::time::TimeProvider; fn build_lsps1_configs( supported_options: LSPS1Options, ) -> (LiquidityServiceConfig, LiquidityClientConfig) { - let lsps1_service_config = LSPS1ServiceConfig { token: None, supported_options }; + let lsps1_service_config = LSPS1ServiceConfig { supported_options }; let service_config = LiquidityServiceConfig { lsps1_service_config: Some(lsps1_service_config), lsps2_service_config: None, @@ -281,7 +281,6 @@ fn lsps1_service_handler_persistence_across_restarts() { let service_config = LiquidityServiceConfig { lsps1_service_config: Some(LSPS1ServiceConfig { supported_options: supported_options.clone(), - token: None, }), lsps2_service_config: None, lsps5_service_config: None, From 03ca5b51bb7dd85fee040012626629678d3e8aa2 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 15:25:55 +0100 Subject: [PATCH 24/27] Add test case for `invalid_token_provided` flow We test the just-added API. Co-authored by Claude AI --- .../tests/lsps1_integration_tests.rs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index ebac780b939..0e048f49a83 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -501,3 +501,99 @@ fn lsps1_service_handler_persistence_across_restarts() { } } } + +#[test] +fn lsps1_invalid_token_error() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, supported_options.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Create an order with an invalid token + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: Some("invalid_token".to_string()), + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + // Service receives the create_order request + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + // Service emits RequestForPaymentDetails event + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id, + order, + }) = request_for_payment_event + { + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(order, order_params); + request_id + } else { + panic!("Unexpected event: expected RequestForPaymentDetails"); + }; + + // Service rejects the order due to invalid token + service_handler.invalid_token_provided(client_node_id, request_id).unwrap(); + + // Get the error response message + let error_response = get_lsps_message!(service_node, client_node_id); + + // Client receives the error response + client_node + .liquidity_manager + .handle_custom_message(error_response, service_node_id) + .unwrap_err(); + + // Client receives OrderRequestFailed event with error code 102 + let error_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderRequestFailed { + request_id, + counterparty_node_id, + error, + }) = error_event + { + assert_eq!(request_id, create_order_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(error.code, 102); // LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE + } else { + panic!("Unexpected event: expected OrderRequestFailed"); + } +} From 6c8f2fd26cb0d91a313f6c525da1f7a374ae07d3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 10:45:39 +0100 Subject: [PATCH 25/27] Drop `lsps1_service` cfg flag --- ci/ci-tests.sh | 2 -- lightning-liquidity/Cargo.toml | 1 - lightning-liquidity/src/events/mod.rs | 2 -- lightning-liquidity/src/lsps1/event.rs | 1 - lightning-liquidity/src/lsps1/mod.rs | 2 -- lightning-liquidity/src/lsps1/msgs.rs | 2 -- lightning-liquidity/src/manager.rs | 25 +++---------------- lightning-liquidity/src/persist.rs | 2 -- .../tests/lsps0_integration_tests.rs | 13 ---------- .../tests/lsps1_integration_tests.rs | 2 +- .../tests/lsps2_integration_tests.rs | 2 -- .../tests/lsps5_integration_tests.rs | 3 --- 12 files changed, 5 insertions(+), 52 deletions(-) diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 488c5ac4826..33c2aae9e2a 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -144,6 +144,4 @@ RUSTFLAGS="--cfg=taproot" cargo test --verbose --color always -p lightning [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean RUSTFLAGS="--cfg=simple_close" cargo test --verbose --color always -p lightning [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -RUSTFLAGS="--cfg=lsps1_service" cargo test --verbose --color always -p lightning-liquidity -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean RUSTFLAGS="--cfg=peer_storage" cargo test --verbose --color always -p lightning diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index 2f83077cabc..c2f0fc06a38 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -47,7 +47,6 @@ parking_lot = { version = "0.12", default-features = false } level = "forbid" # When adding a new cfg attribute, ensure that it is added to this list. check-cfg = [ - "cfg(lsps1_service)", "cfg(c_bindings)", "cfg(backtrace)", "cfg(ldk_bench)", diff --git a/lightning-liquidity/src/events/mod.rs b/lightning-liquidity/src/events/mod.rs index c39b8b9fd59..3d9587a058a 100644 --- a/lightning-liquidity/src/events/mod.rs +++ b/lightning-liquidity/src/events/mod.rs @@ -33,7 +33,6 @@ pub enum LiquidityEvent { /// An LSPS1 (Channel Request) client event. LSPS1Client(lsps1::event::LSPS1ClientEvent), /// An LSPS1 (Channel Request) server event. - #[cfg(lsps1_service)] LSPS1Service(lsps1::event::LSPS1ServiceEvent), /// An LSPS2 (JIT Channel) client event. LSPS2Client(lsps2::event::LSPS2ClientEvent), @@ -57,7 +56,6 @@ impl From for LiquidityEvent { } } -#[cfg(lsps1_service)] impl From for LiquidityEvent { fn from(event: lsps1::event::LSPS1ServiceEvent) -> Self { Self::LSPS1Service(event) diff --git a/lightning-liquidity/src/lsps1/event.rs b/lightning-liquidity/src/lsps1/event.rs index d966f8bdc2f..7a9ef7ee991 100644 --- a/lightning-liquidity/src/lsps1/event.rs +++ b/lightning-liquidity/src/lsps1/event.rs @@ -142,7 +142,6 @@ pub enum LSPS1ClientEvent { } /// An event which an LSPS1 server should take some action in response to. -#[cfg(lsps1_service)] #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS1ServiceEvent { /// A client has selected the parameters to use from the supported options of the LSP diff --git a/lightning-liquidity/src/lsps1/mod.rs b/lightning-liquidity/src/lsps1/mod.rs index 2270abe2fa3..5f7f554dfb0 100644 --- a/lightning-liquidity/src/lsps1/mod.rs +++ b/lightning-liquidity/src/lsps1/mod.rs @@ -12,7 +12,5 @@ pub mod client; pub mod event; pub mod msgs; -#[cfg(lsps1_service)] pub(crate) mod peer_state; -#[cfg(lsps1_service)] pub mod service; diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs index a2382e0b71c..eae9568f589 100644 --- a/lightning-liquidity/src/lsps1/msgs.rs +++ b/lightning-liquidity/src/lsps1/msgs.rs @@ -31,9 +31,7 @@ pub(crate) const LSPS1_CREATE_ORDER_METHOD_NAME: &str = "lsps1.create_order"; pub(crate) const LSPS1_GET_ORDER_METHOD_NAME: &str = "lsps1.get_order"; pub(crate) const _LSPS1_CREATE_ORDER_REQUEST_INVALID_PARAMS_ERROR_CODE: i32 = -32602; -#[cfg(lsps1_service)] pub(crate) const LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE: i32 = 100; -#[cfg(lsps1_service)] pub(crate) const LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE: i32 = 101; pub(crate) const LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE: i32 = 102; diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 96aa8055853..d2370b78c36 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -23,15 +23,13 @@ use crate::lsps5::client::{LSPS5ClientConfig, LSPS5ClientHandler}; use crate::lsps5::msgs::LSPS5Message; use crate::lsps5::service::{LSPS5ServiceConfig, LSPS5ServiceHandler}; use crate::message_queue::MessageQueue; -#[cfg(lsps1_service)] -use crate::persist::read_lsps1_service_peer_states; use crate::persist::{ - read_event_queue, read_lsps2_service_peer_states, read_lsps5_service_peer_states, + read_event_queue, read_lsps1_service_peer_states, read_lsps2_service_peer_states, + read_lsps5_service_peer_states, }; use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler}; use crate::lsps1::msgs::LSPS1Message; -#[cfg(lsps1_service)] use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler, LSPS1ServiceHandlerSync}; use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; @@ -73,7 +71,6 @@ const LSPS_FEATURE_BIT: usize = 729; #[derive(Clone)] pub struct LiquidityServiceConfig { /// Optional server-side configuration for LSPS1 channel requests. - #[cfg(lsps1_service)] pub lsps1_service_config: Option, /// Optional server-side configuration for JIT channels /// should you want to support them. @@ -301,7 +298,6 @@ pub struct LiquidityManager< ignored_peers: RwLock>, lsps0_client_handler: LSPS0ClientHandler, lsps0_service_handler: Option, - #[cfg(lsps1_service)] lsps1_service_handler: Option>, lsps1_client_handler: Option>, lsps2_service_handler: Option>, @@ -476,7 +472,6 @@ where }) }); - #[cfg(lsps1_service)] let lsps1_service_handler = if let Some(service_config) = service_config.as_ref() { if let Some(lsps1_service_config) = service_config.lsps1_service_config.as_ref() { if let Some(number) = @@ -524,7 +519,6 @@ where lsps0_client_handler, lsps0_service_handler, lsps1_client_handler, - #[cfg(lsps1_service)] lsps1_service_handler, lsps2_client_handler, lsps2_service_handler, @@ -555,7 +549,6 @@ where } /// Returns a reference to the LSPS1 server-side handler. - #[cfg(lsps1_service)] pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler> { self.lsps1_service_handler.as_ref() } @@ -659,7 +652,6 @@ where let mut did_persist = false; did_persist |= self.pending_events.persist().await?; - #[cfg(lsps1_service)] if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() { did_persist |= lsps1_service_handler.persist().await?; } @@ -705,18 +697,15 @@ where }, } }, - LSPSMessage::LSPS1(_msg @ LSPS1Message::Request(..)) => { - #[cfg(lsps1_service)] + LSPSMessage::LSPS1(msg @ LSPS1Message::Request(..)) => { match &self.lsps1_service_handler { Some(lsps1_service_handler) => { - lsps1_service_handler.handle_message(_msg, sender_node_id)?; + lsps1_service_handler.handle_message(msg, sender_node_id)?; }, None => { return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node {}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Debug)}); }, } - #[cfg(not(lsps1_service))] - return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node {}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Debug)}); }, LSPSMessage::LSPS2(msg @ LSPS2Message::Response(..)) => { match &self.lsps2_client_handler { @@ -757,14 +746,10 @@ where .lsps2_service_handler .as_ref() .is_some_and(|h| h.has_active_requests(sender_node_id)); - #[cfg(lsps1_service)] let lsps1_has_active_requests = self .lsps1_service_handler .as_ref() .is_some_and(|h| h.has_active_requests(sender_node_id)); - #[cfg(not(lsps1_service))] - let lsps1_has_active_requests = false; - lsps5_service_handler.enforce_prior_activity_or_reject( sender_node_id, lsps2_has_active_requests, @@ -928,7 +913,6 @@ where // If the peer was misbehaving, drop it from the ignored list to cleanup the kept state. self.ignored_peers.write().unwrap().remove(&counterparty_node_id); - #[cfg(lsps1_service)] if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() { lsps1_service_handler.peer_disconnected(counterparty_node_id); } @@ -1093,7 +1077,6 @@ where /// Returns a reference to the LSPS1 server-side handler. /// /// Wraps [`LiquidityManager::lsps1_service_handler`]. - #[cfg(lsps1_service)] pub fn lsps1_service_handler<'a>( &'a self, ) -> Option, TP>> { diff --git a/lightning-liquidity/src/persist.rs b/lightning-liquidity/src/persist.rs index 48ecc3b18dd..4cbdd35f7bd 100644 --- a/lightning-liquidity/src/persist.rs +++ b/lightning-liquidity/src/persist.rs @@ -10,7 +10,6 @@ //! Types and utils for persistence. use crate::events::{EventQueueDeserWrapper, LiquidityEvent}; -#[cfg(lsps1_service)] use crate::lsps1::peer_state::PeerState as LSPS1ServicePeerState; use crate::lsps2::service::PeerState as LSPS2ServicePeerState; use crate::lsps5::service::PeerState as LSPS5ServicePeerState; @@ -92,7 +91,6 @@ where Ok(Some(queue.0)) } -#[cfg(lsps1_service)] pub(crate) async fn read_lsps1_service_peer_states( kv_store: K, ) -> Result>, lightning::io::Error> diff --git a/lightning-liquidity/tests/lsps0_integration_tests.rs b/lightning-liquidity/tests/lsps0_integration_tests.rs index 58d9e867398..c2e94e30661 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -6,11 +6,8 @@ use common::{create_service_and_client_nodes, get_lsps_message, LSPSNodes}; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::event::LSPS0ClientEvent; -#[cfg(lsps1_service)] use lightning_liquidity::lsps1::client::LSPS1ClientConfig; -#[cfg(lsps1_service)] use lightning_liquidity::lsps1::msgs::LSPS1Options; -#[cfg(lsps1_service)] use lightning_liquidity::lsps1::service::LSPS1ServiceConfig; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; @@ -35,7 +32,6 @@ fn list_protocols_integration_test() { let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let promise_secret = [42; 32]; let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; - #[cfg(lsps1_service)] let lsps1_service_config = { let supported_options = LSPS1Options { min_required_channel_confirmations: 0, @@ -53,7 +49,6 @@ fn list_protocols_integration_test() { }; let lsps5_service_config = LSPS5ServiceConfig::default(); let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: Some(lsps1_service_config), lsps2_service_config: Some(lsps2_service_config), lsps5_service_config: Some(lsps5_service_config), @@ -61,14 +56,10 @@ fn list_protocols_integration_test() { }; let lsps2_client_config = LSPS2ClientConfig::default(); - #[cfg(lsps1_service)] let lsps1_client_config: LSPS1ClientConfig = LSPS1ClientConfig { max_channel_fees_msat: None }; let lsps5_client_config = LSPS5ClientConfig::default(); let client_config = LiquidityClientConfig { - #[cfg(lsps1_service)] lsps1_client_config: Some(lsps1_client_config), - #[cfg(not(lsps1_service))] - lsps1_client_config: None, lsps2_client_config: Some(lsps2_client_config), lsps5_client_config: Some(lsps5_client_config), }; @@ -107,16 +98,12 @@ fn list_protocols_integration_test() { protocols, }) => { assert_eq!(counterparty_node_id, client_node_id); - #[cfg(lsps1_service)] { assert!(protocols.contains(&1)); assert!(protocols.contains(&2)); assert!(protocols.contains(&5)); assert_eq!(protocols.len(), 3); } - - #[cfg(not(lsps1_service))] - assert_eq!(protocols, vec![2, 5]); }, _ => panic!("Unexpected event"), } diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs index 0e048f49a83..b4ab0a692bc 100644 --- a/lightning-liquidity/tests/lsps1_integration_tests.rs +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -1,4 +1,4 @@ -#![cfg(all(test, feature = "time", lsps1_service))] +#![cfg(all(test, feature = "time"))] mod common; diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 2900302c0ea..e4317f20ed1 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -61,7 +61,6 @@ fn build_lsps2_configs() -> ([u8; 32], LiquidityServiceConfig, LiquidityClientCo let promise_secret = [42; 32]; let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(lsps2_service_config), lsps5_service_config: None, @@ -946,7 +945,6 @@ fn lsps2_service_handler_persistence_across_restarts() { let promise_secret = [42; 32]; let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(LSPS2ServiceConfig { promise_secret }), lsps5_service_config: None, diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs index 6af0c137be5..2b32b4dcbc6 100644 --- a/lightning-liquidity/tests/lsps5_integration_tests.rs +++ b/lightning-liquidity/tests/lsps5_integration_tests.rs @@ -52,7 +52,6 @@ pub(crate) fn lsps5_test_setup_with_kv_stores<'a, 'b, 'c>( ) -> (LSPSNodes<'a, 'b, 'c>, LSPS5Validator) { let lsps5_service_config = LSPS5ServiceConfig::default(); let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: None, lsps5_service_config: Some(lsps5_service_config), @@ -236,7 +235,6 @@ pub(crate) fn lsps5_lsps2_test_setup<'a, 'b, 'c>( let lsps5_service_config = LSPS5ServiceConfig::default(); let lsps2_service_config = LSPS2ServiceConfig { promise_secret: [42; 32] }; let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(lsps2_service_config), lsps5_service_config: Some(lsps5_service_config), @@ -1512,7 +1510,6 @@ fn lsps5_service_handler_persistence_across_restarts() { let client_kv_store = Arc::new(TestStore::new(false)); let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: None, lsps5_service_config: Some(LSPS5ServiceConfig::default()), From 72b90957b48be4ef6cb3120302fa56c122d929ed Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 16:53:40 +0100 Subject: [PATCH 26/27] Bump `lightning-liquidity` version number .. to make SemVer checks happy. --- lightning-background-processor/Cargo.toml | 4 ++-- lightning-liquidity/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lightning-background-processor/Cargo.toml b/lightning-background-processor/Cargo.toml index ef0a9840613..7fe68bc1933 100644 --- a/lightning-background-processor/Cargo.toml +++ b/lightning-background-processor/Cargo.toml @@ -26,14 +26,14 @@ bitcoin_hashes = { version = "0.14.0", default-features = false } bitcoin-io = { version = "0.1.2", default-features = false } lightning = { version = "0.3.0", path = "../lightning", default-features = false } lightning-rapid-gossip-sync = { version = "0.2.0", path = "../lightning-rapid-gossip-sync", default-features = false } -lightning-liquidity = { version = "0.2.0", path = "../lightning-liquidity", default-features = false } +lightning-liquidity = { version = "0.3.0", path = "../lightning-liquidity", default-features = false } possiblyrandom = { version = "0.2", path = "../possiblyrandom", default-features = false } [dev-dependencies] tokio = { version = "1.35", features = [ "macros", "rt", "rt-multi-thread", "sync", "time" ] } lightning = { version = "0.3.0", path = "../lightning", features = ["_test_utils"] } lightning-invoice = { version = "0.34.0", path = "../lightning-invoice" } -lightning-liquidity = { version = "0.2.0", path = "../lightning-liquidity", default-features = false, features = ["_test_utils"] } +lightning-liquidity = { version = "0.3.0", path = "../lightning-liquidity", default-features = false, features = ["_test_utils"] } lightning-persister = { version = "0.2.0", path = "../lightning-persister" } [lints] diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index c2f0fc06a38..19c535b0aee 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-liquidity" -version = "0.2.0+git" +version = "0.3.0+git" authors = ["John Cantrell ", "Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" From c6eb6b3e9f78d8dfa53d7a3f4e12bbacc88115b9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Dec 2025 17:04:09 +0100 Subject: [PATCH 27/27] Fix clippy lints --- lightning-liquidity/src/lsps1/service.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index 0294d216182..2634b755962 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -291,7 +291,7 @@ where if !is_valid(¶ms.order, &self.config.supported_options) { let response = LSPS1Response::CreateOrderError(LSPSResponseError { code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, - message: format!("Order does not match options supported by LSP server"), + message: "Order does not match options supported by LSP server".to_string(), data: Some(format!("Supported options are {:?}", &self.config.supported_options)), }); let msg = LSPS1Message::Response(request_id, response).into(); @@ -478,7 +478,8 @@ where let order = peer_state_lock.get_order(¶ms.order_id).map_err(|e| { let response = LSPS1Response::GetOrderError(LSPSResponseError { code: LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, - message: format!("Order with the requested order_id has not been found."), + message: "Order with the requested order_id has not been found." + .to_string(), data: None, }); let msg = LSPS1Message::Response(request_id.clone(), response).into(); @@ -503,7 +504,7 @@ where None => { let response = LSPS1Response::GetOrderError(LSPSResponseError { code: LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, - message: format!("Order with the requested order_id has not been found."), + message: "Order with the requested order_id has not been found.".to_string(), data: None, }); let msg = LSPS1Message::Response(request_id, response).into();