From 4e772099eb4ec50947220da2a3a27887e71d5aaa Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 May 2025 10:02:05 -0300 Subject: [PATCH 1/5] Prefactor -> document LSPSRequestId usage on select_opening_params --- lightning-liquidity/src/lsps2/client.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lightning-liquidity/src/lsps2/client.rs b/lightning-liquidity/src/lsps2/client.rs index 3dabb83c954..ced7b07dd97 100644 --- a/lightning-liquidity/src/lsps2/client.rs +++ b/lightning-liquidity/src/lsps2/client.rs @@ -150,7 +150,9 @@ where /// but MPP can no longer be used to pay it. /// /// The client agrees to paying an opening fee equal to - /// `max(min_fee_msat, proportional*(payment_size_msat/1_000_000))`. + /// `max(min_fee_msat, proportional * (payment_size_msat / 1_000_000))`. + /// + /// Returns the used [`LSPSRequestId`] that was used for the buy request. /// /// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady /// [`InvoiceParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::InvoiceParametersReady From 1ee90ac82b7903fee40af16683e3bf1182899671 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Tue, 27 May 2025 13:14:52 -0300 Subject: [PATCH 2/5] Add error handling events for failed client requests Add GetInfoFailed and BuyRequestFailed event variants to LSPS2ClientEvent to handle LSP error responses, similar to the OrderRequestFailed event added to LSPS1. Fixes #3459 --- lightning-liquidity/src/lsps2/event.rs | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lightning-liquidity/src/lsps2/event.rs b/lightning-liquidity/src/lsps2/event.rs index f20a74e199a..f5e42665b5c 100644 --- a/lightning-liquidity/src/lsps2/event.rs +++ b/lightning-liquidity/src/lsps2/event.rs @@ -10,7 +10,7 @@ //! Contains bLIP-52 / LSPS2 event types use super::msgs::LSPS2OpeningFeeParams; -use crate::lsps0::ser::LSPSRequestId; +use crate::lsps0::ser::{LSPSRequestId, LSPSResponseError}; use alloc::string::String; use alloc::vec::Vec; @@ -61,6 +61,40 @@ pub enum LSPS2ClientEvent { /// The initial payment size you specified. payment_size_msat: Option, }, + /// A request previously issued via [`LSPS2ClientHandler::request_opening_params`] + /// failed as the LSP returned an error response. + /// + /// [`LSPS2ClientHandler::request_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::request_opening_params + GetInfoFailed { + /// The identifier of the issued LSPS2 `get_info` request, as returned by + /// [`LSPS2ClientHandler::request_opening_params`]. + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS2ClientHandler::request_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::request_opening_params + request_id: LSPSRequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The error that was returned. + error: LSPSResponseError, + }, + /// A request previously issued via [`LSPS2ClientHandler::select_opening_params`] + /// failed as the LSP returned an error response. + /// + /// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params + BuyRequestFailed { + /// The identifier of the issued LSPS2 `buy` request, as returned by + /// [`LSPS2ClientHandler::select_opening_params`]. + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params + request_id: LSPSRequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The error that was returned. + error: LSPSResponseError, + }, } /// An event which an bLIP-52 / LSPS2 server should take some action in response to. From 50f313008c09a4eece3f0820a5aafa93fd7fd01e Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Tue, 27 May 2025 13:15:51 -0300 Subject: [PATCH 3/5] Emit error events and return errors in client error handlers Update handle_get_info_error and handle_buy_error to: - Emit GetInfoFailed and BuyRequestFailed events to notify users - Return LightningError instead of Ok() to properly signal failures - Use the actual error parameter instead of ignoring it This ensures consistent error handling behavior across LSPS implementations. --- lightning-liquidity/src/lsps2/client.rs | 38 ++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/lightning-liquidity/src/lsps2/client.rs b/lightning-liquidity/src/lsps2/client.rs index ced7b07dd97..5b7e0902e31 100644 --- a/lightning-liquidity/src/lsps2/client.rs +++ b/lightning-liquidity/src/lsps2/client.rs @@ -232,8 +232,9 @@ where fn handle_get_info_error( &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, - _error: LSPSResponseError, + error: LSPSResponseError, ) -> Result<(), LightningError> { + let event_queue_notifier = self.pending_events.notifier(); let outer_state_lock = self.per_peer_state.read().unwrap(); match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { @@ -249,7 +250,21 @@ where }); } - Ok(()) + let lightning_error = LightningError { + err: format!( + "Received get_info error response for request {:?}: {:?}", + request_id, error + ), + action: ErrorAction::IgnoreAndLog(Level::Error), + }; + + event_queue_notifier.enqueue(LSPS2ClientEvent::GetInfoFailed { + request_id, + counterparty_node_id: *counterparty_node_id, + error, + }); + + Err(lightning_error) }, None => { return Err(LightningError { err: format!("Received error response for a get_info request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); @@ -310,8 +325,9 @@ where fn handle_buy_error( &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, - _error: LSPSResponseError, + error: LSPSResponseError, ) -> Result<(), LightningError> { + let event_queue_notifier = self.pending_events.notifier(); let outer_state_lock = self.per_peer_state.read().unwrap(); match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { @@ -322,7 +338,21 @@ where action: ErrorAction::IgnoreAndLog(Level::Info), })?; - Ok(()) + let lightning_error = LightningError { + err: format!( + "Received buy error response for request {:?}: {:?}", + request_id, error + ), + action: ErrorAction::IgnoreAndLog(Level::Error), + }; + + event_queue_notifier.enqueue(LSPS2ClientEvent::BuyRequestFailed { + request_id, + counterparty_node_id: *counterparty_node_id, + error, + }); + + Err(lightning_error) }, None => { return Err(LightningError { err: format!("Received error response for a buy request from an unknown counterparty ({:?})", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); From 0d0dbc4ea749f1a44ee21a20ffacb84156dde7d1 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Tue, 27 May 2025 13:16:57 -0300 Subject: [PATCH 4/5] Fix error response types for rate limiting in service Replace hardcoded BuyError responses with appropriate error types based on the actual request (GetInfo vs Buy) when rate limiting is triggered. This fixes incorrect error response types and provides an easy way to test the error event handling added in this PR. --- lightning-liquidity/src/lsps2/service.rs | 43 ++++++++++++------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 71286ef9f69..abab51366ff 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -1348,22 +1348,32 @@ where &self, peer_state_lock: &mut MutexGuard<'a, PeerState>, request_id: LSPSRequestId, counterparty_node_id: PublicKey, request: LSPS2Request, ) -> (Result<(), LightningError>, Option) { - if self.total_pending_requests.load(Ordering::Relaxed) >= MAX_TOTAL_PENDING_REQUESTS { - let response = LSPS2Response::BuyError(LSPSResponseError { + let create_pending_request_limit_exceeded_response = |error_message: String| { + let error_details = LSPSResponseError { code: LSPS0_CLIENT_REJECTED_ERROR_CODE, message: "Reached maximum number of pending requests. Please try again later." .to_string(), data: None, + }; + let response = match &request { + LSPS2Request::GetInfo(_) => LSPS2Response::GetInfoError(error_details), + LSPS2Request::Buy(_) => LSPS2Response::BuyError(error_details), + }; + let msg = Some(LSPS2Message::Response(request_id.clone(), response).into()); + + let result = Err(LightningError { + err: error_message, + action: ErrorAction::IgnoreAndLog(Level::Debug), }); - let msg = Some(LSPS2Message::Response(request_id, response).into()); + (result, msg) + }; - let err = format!( - "Peer {} reached maximum number of total pending requests: {}", - counterparty_node_id, MAX_TOTAL_PENDING_REQUESTS + if self.total_pending_requests.load(Ordering::Relaxed) >= MAX_TOTAL_PENDING_REQUESTS { + let error_message = format!( + "Reached maximum number of total pending requests: {}", + MAX_TOTAL_PENDING_REQUESTS ); - let result = - Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); - return (result, msg); + return create_pending_request_limit_exceeded_response(error_message); } if peer_state_lock.pending_requests_and_channels() < MAX_PENDING_REQUESTS_PER_PEER { @@ -1371,22 +1381,11 @@ where self.total_pending_requests.fetch_add(1, Ordering::Relaxed); (Ok(()), None) } else { - let response = LSPS2Response::BuyError(LSPSResponseError { - code: LSPS0_CLIENT_REJECTED_ERROR_CODE, - message: "Reached maximum number of pending requests. Please try again later." - .to_string(), - data: None, - }); - let msg = Some(LSPS2Message::Response(request_id, response).into()); - - let err = format!( + let error_message = format!( "Peer {} reached maximum number of pending requests: {}", counterparty_node_id, MAX_PENDING_REQUESTS_PER_PEER ); - let result = - Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); - - (result, msg) + create_pending_request_limit_exceeded_response(error_message) } } From cea624ebcd8ba9fcc85207f2b43241c43793adef Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Tue, 27 May 2025 13:17:54 -0300 Subject: [PATCH 5/5] Add integration tests for error event handling Add tests for the new error events: - Test per-peer request limit rejection (GetInfoFailed) - Test global request limit rejection (BuyRequestFailed) - Test invalid token handling (GetInfoFailed) These tests verify that error events are properly emitted when LSP requests fail, ensuring the error handling behavior works end-to-end. --- .../tests/lsps2_integration_tests.rs | 374 ++++++++++++++---- 1 file changed, 308 insertions(+), 66 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 62f9f2e0a03..ef88d6220a4 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -2,12 +2,14 @@ mod common; -use common::{create_service_and_client_nodes, get_lsps_message, Node}; +use common::create_service_and_client_nodes; +use common::{get_lsps_message, Node}; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::ser::LSPSDateTime; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; -use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::event::LSPS2ClientEvent; +use lightning_liquidity::lsps2::event::LSPS2ServiceEvent; use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::is_valid_opening_fee_params; @@ -31,7 +33,11 @@ use bitcoin::Network; use std::str::FromStr; use std::time::Duration; +const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; +const MAX_TOTAL_PENDING_REQUESTS: usize = 1000; + fn setup_test_lsps2( + persist_dir: &str, ) -> (bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey, Node, Node, [u8; 32]) { let promise_secret = [42; 32]; let signing_key = SecretKey::from_slice(&promise_secret).unwrap(); @@ -50,7 +56,7 @@ fn setup_test_lsps2( }; let (service_node, client_node) = - create_service_and_client_nodes("webhook_registration_flow", service_config, client_config); + create_service_and_client_nodes(persist_dir, service_config, client_config); let secp = bitcoin::secp256k1::Secp256k1::new(); let service_node_id = bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &signing_key); @@ -114,7 +120,7 @@ fn create_jit_invoice( #[test] fn invoice_generation_flow() { let (service_node_id, client_node_id, service_node, client_node, promise_secret) = - setup_test_lsps2(); + setup_test_lsps2("invoice_generation_flow"); let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); @@ -125,17 +131,17 @@ fn invoice_generation_flow() { service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap(); let get_info_event = service_node.liquidity_manager.next_event().unwrap(); - match get_info_event { - LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { - request_id, - counterparty_node_id, - token, - }) => { - assert_eq!(request_id, get_info_request_id); - assert_eq!(counterparty_node_id, client_node_id); - assert_eq!(token, None); - }, - _ => panic!("Unexpected event"), + if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) = get_info_event + { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(token, None); + } else { + panic!("Unexpected event"); } let raw_opening_params = LSPS2RawOpeningFeeParams { @@ -187,19 +193,19 @@ fn invoice_generation_flow() { service_node.liquidity_manager.handle_custom_message(buy_request, client_node_id).unwrap(); let buy_event = service_node.liquidity_manager.next_event().unwrap(); - match buy_event { - LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { - request_id, - counterparty_node_id, - opening_fee_params: ofp, - payment_size_msat: psm, - }) => { - assert_eq!(request_id, buy_request_id); - assert_eq!(counterparty_node_id, client_node_id); - assert_eq!(opening_fee_params, ofp); - assert_eq!(payment_size_msat, psm); - }, - _ => panic!("Unexpected event"), + if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: ofp, + payment_size_msat: psm, + }) = buy_event + { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(opening_fee_params, ofp); + assert_eq!(payment_size_msat, psm); + } else { + panic!("Unexpected event"); } let user_channel_id = 42; @@ -222,22 +228,22 @@ fn invoice_generation_flow() { client_node.liquidity_manager.handle_custom_message(buy_response, service_node_id).unwrap(); let invoice_params_event = client_node.liquidity_manager.next_event().unwrap(); - match invoice_params_event { - LiquidityEvent::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { - request_id, - counterparty_node_id, - intercept_scid: iscid, - cltv_expiry_delta: ced, - payment_size_msat: psm, - }) => { - assert_eq!(request_id, buy_request_id); - assert_eq!(counterparty_node_id, service_node_id); - assert_eq!(intercept_scid, iscid); - assert_eq!(cltv_expiry_delta, ced); - assert_eq!(payment_size_msat, psm); - }, - _ => panic!("Unexpected event"), - }; + if let LiquidityEvent::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { + request_id, + counterparty_node_id, + intercept_scid: iscid, + cltv_expiry_delta: ced, + payment_size_msat: psm, + }) = invoice_params_event + { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(intercept_scid, iscid); + assert_eq!(cltv_expiry_delta, ced); + assert_eq!(payment_size_msat, psm); + } else { + panic!("Unexpected event"); + } let description = "asdf"; let expiry_secs = 3600; @@ -255,15 +261,13 @@ fn invoice_generation_flow() { #[test] fn channel_open_failed() { - let (service_node_id, client_node_id, service_node, client_node, _) = setup_test_lsps2(); + let (service_node_id, client_node_id, service_node, client_node, _) = + setup_test_lsps2("channel_open_failed"); let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); - let get_info_request_id = client_node - .liquidity_manager - .lsps2_client_handler() - .unwrap() - .request_opening_params(service_node_id, None); + let get_info_request_id = client_handler.request_opening_params(service_node_id, None); let get_info_request = get_lsps_message!(client_node, service_node_id); service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap(); @@ -301,10 +305,7 @@ fn channel_open_failed() { }; let payment_size_msat = Some(1_000_000); - let buy_request_id = client_node - .liquidity_manager - .lsps2_client_handler() - .unwrap() + let buy_request_id = client_handler .select_opening_params(service_node_id, payment_size_msat, opening_fee_params.clone()) .unwrap(); let buy_request = get_lsps_message!(client_node, service_node_id); @@ -388,7 +389,8 @@ fn channel_open_failed() { #[test] fn channel_open_failed_nonexistent_channel() { - let (_, client_node_id, service_node, _, _) = setup_test_lsps2(); + let (_, client_node_id, service_node, _, _) = + setup_test_lsps2("channel_open_failed_nonexistent_channel"); let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); @@ -407,16 +409,14 @@ fn channel_open_failed_nonexistent_channel() { #[test] fn channel_open_abandoned() { - let (service_node_id, client_node_id, service_node, client_node, _) = setup_test_lsps2(); + let (service_node_id, client_node_id, service_node, client_node, _) = + setup_test_lsps2("channel_open_abandoned"); let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); // Set up a JIT channel - let get_info_request_id = client_node - .liquidity_manager - .lsps2_client_handler() - .unwrap() - .request_opening_params(service_node_id, None); + let get_info_request_id = client_handler.request_opening_params(service_node_id, None); let get_info_request = get_lsps_message!(client_node, service_node_id); service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap(); let _get_info_event = service_node.liquidity_manager.next_event().unwrap(); @@ -453,10 +453,7 @@ fn channel_open_abandoned() { }; let payment_size_msat = Some(1_000_000); - let buy_request_id = client_node - .liquidity_manager - .lsps2_client_handler() - .unwrap() + let buy_request_id = client_handler .select_opening_params(service_node_id, payment_size_msat, opening_fee_params.clone()) .unwrap(); let buy_request = get_lsps_message!(client_node, service_node_id); @@ -489,7 +486,8 @@ fn channel_open_abandoned() { #[test] fn channel_open_abandoned_nonexistent_channel() { - let (_, client_node_id, service_node, _, _) = setup_test_lsps2(); + let (_, client_node_id, service_node, _, _) = + setup_test_lsps2("channel_open_abandoned_nonexistent_channel"); let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); // Call channel_open_abandoned with a nonexistent user_channel_id @@ -504,3 +502,247 @@ fn channel_open_abandoned_nonexistent_channel() { other => panic!("Unexpected error type: {:?}", other), } } + +#[test] +fn max_pending_requests_per_peer_rejected() { + let (service_node_id, client_node_id, service_node, client_node, _) = + setup_test_lsps2("max_pending_requests_per_peer_rejected"); + + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); + + for _ in 0..MAX_PENDING_REQUESTS_PER_PEER { + let _ = client_handler.request_opening_params(service_node_id, None); + let req_msg = get_lsps_message!(client_node, service_node_id); + let result = service_node.liquidity_manager.handle_custom_message(req_msg, client_node_id); + assert!(result.is_ok()); + let event = service_node.liquidity_manager.next_event().unwrap(); + match event { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { .. }) => {}, + _ => panic!("Unexpected event"), + } + } + + // Test per-peer limit: the next request should be rejected + let rejected_req_id = client_handler.request_opening_params(service_node_id, None); + let rejected_req_msg = get_lsps_message!(client_node, service_node_id); + + let result = + service_node.liquidity_manager.handle_custom_message(rejected_req_msg, client_node_id); + assert!(result.is_err(), "We should have hit the per-peer limit"); + + let get_info_error_response = get_lsps_message!(service_node, client_node_id); + let result = client_node + .liquidity_manager + .handle_custom_message(get_info_error_response, service_node_id); + assert!(result.is_err()); + + let event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Client(LSPS2ClientEvent::GetInfoFailed { + request_id, + counterparty_node_id, + error, + }) = event + { + assert_eq!(request_id, rejected_req_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(error.code, 1); // LSPS0_CLIENT_REJECTED_ERROR_CODE + } else { + panic!("Expected LSPS2ClientEvent::GetInfoFailed event"); + } +} + +#[test] +fn max_total_requests_buy_rejected() { + let (service_node_id, _, service_node, client_node, _) = + setup_test_lsps2("max_total_requests_buy_rejected"); + + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + let secp = Secp256k1::new(); + + let special_sk_bytes = [99u8; 32]; + let special_sk = SecretKey::from_slice(&special_sk_bytes).unwrap(); + let special_node_id = PublicKey::from_secret_key(&secp, &special_sk); + + let _ = client_handler.request_opening_params(service_node_id, None); + let get_info_request = get_lsps_message!(client_node, service_node_id); + service_node + .liquidity_manager + .handle_custom_message(get_info_request, special_node_id) + .unwrap(); + + let get_info_event = service_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { request_id, .. }) = + get_info_event + { + let raw_opening_params = LSPS2RawOpeningFeeParams { + min_fee_msat: 100, + proportional: 21, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 144, + max_client_to_self_delay: 128, + min_payment_size_msat: 1, + max_payment_size_msat: 100_000_000, + }; + + service_handler + .opening_fee_params_generated(&special_node_id, request_id, vec![raw_opening_params]) + .unwrap(); + } else { + panic!("Unexpected event"); + } + + let get_info_response = get_lsps_message!(service_node, special_node_id); + client_node + .liquidity_manager + .handle_custom_message(get_info_response, service_node_id) + .unwrap(); + + let opening_params_event = client_node.liquidity_manager.next_event().unwrap(); + let opening_fee_params = match opening_params_event { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { + opening_fee_params_menu, + .. + }) => opening_fee_params_menu.first().unwrap().clone(), + _ => panic!("Unexpected event"), + }; + + // Now fill up the global limit with additional GetInfo requests from other peers + let mut filled = 0; + let mut peer_idx = 0; + + while filled < MAX_TOTAL_PENDING_REQUESTS { + let sk_bytes = [peer_idx as u8 + 1; 32]; + let sk = SecretKey::from_slice(&sk_bytes).unwrap(); + let peer_node_id = PublicKey::from_secret_key(&secp, &sk); + + // Skip if this is our special node + if peer_node_id == special_node_id { + peer_idx += 1; + continue; + } + + for _ in 0..MAX_PENDING_REQUESTS_PER_PEER { + if filled >= MAX_TOTAL_PENDING_REQUESTS { + break; + } + + let _ = client_handler.request_opening_params(service_node_id, None); + let req_msg = get_lsps_message!(client_node, service_node_id); + let result = + service_node.liquidity_manager.handle_custom_message(req_msg, peer_node_id); + assert!(result.is_ok()); + + let event = service_node.liquidity_manager.next_event().unwrap(); + match event { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { .. }) => {}, + _ => panic!("Unexpected event"), + } + + filled += 1; + } + peer_idx += 1; + } + + // Now try to send a Buy request with our special node, which should be rejected + let payment_size_msat = Some(1_000_000); + let buy_request_id = client_handler + .select_opening_params(service_node_id, payment_size_msat, opening_fee_params) + .unwrap(); + let buy_request = get_lsps_message!(client_node, service_node_id); + + let result = service_node.liquidity_manager.handle_custom_message(buy_request, special_node_id); + assert!(result.is_err(), "The Buy request should have been rejected"); + + let buy_error_response = get_lsps_message!(service_node, special_node_id); + let result = + client_node.liquidity_manager.handle_custom_message(buy_error_response, service_node_id); + assert!(result.is_err()); + + let event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Client(LSPS2ClientEvent::BuyRequestFailed { + request_id, + counterparty_node_id, + error, + }) = event + { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(error.code, 1); // LSPS0_CLIENT_REJECTED_ERROR_CODE + } else { + panic!("Expected LSPS2ClientEvent::BuyRequestFailed event"); + } +} + +#[test] +fn invalid_token_flow() { + let (service_node_id, client_node_id, service_node, client_node, _promise_secret) = + setup_test_lsps2("invalid_token_flow"); + + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + + let token = Some("invalid_token".to_string()); + let get_info_request_id = client_handler.request_opening_params(service_node_id, token); + let get_info_request = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap(); + + let get_info_event = service_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) = get_info_event + { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(token, Some("invalid_token".to_string())); + + // Service rejects the token as invalid + service_handler.invalid_token_provided(&client_node_id, request_id.clone()).unwrap(); + + // Attempt to respond to the same request again which should fail + // because the request has been removed from pending_requests + let raw_opening_params = LSPS2RawOpeningFeeParams { + min_fee_msat: 100, + proportional: 21, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 144, + max_client_to_self_delay: 128, + min_payment_size_msat: 1, + max_payment_size_msat: 100_000_000, + }; + + let result = service_handler.opening_fee_params_generated( + &client_node_id, + request_id.clone(), + vec![raw_opening_params], + ); + + assert!(result.is_err(), "Request should have been removed from pending_requests"); + } else { + panic!("Unexpected event"); + } + + let get_info_error_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(get_info_error_response, service_node_id) + .unwrap_err(); + + let error_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Client(LSPS2ClientEvent::GetInfoFailed { + request_id, + counterparty_node_id, + error, + }) = error_event + { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(error.code, 200); // LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE + } else { + panic!("Expected LSPS2ClientEvent::GetInfoFailed event"); + } +}