Respond to `GetOrder` requests from our saved state
What changed, and why it matters
This commit changes how a Lightning liquidity service responds to order-status lookups. Instead of asking the user/LSP to check payment status every time a client asks, it now answers immediately from saved state and lets the user/LSP push updates only when something actually changes. The change removes one event type and one callback step. It is a design/API refactor, not a clear security fix, though it does add a proper 'order not found' error response for unknown orders.
Treat as a normal API refactor. Review callers that previously handled `CheckPaymentConfirmation` and called `update_order_status` to ensure they migrate to the new push-update model. Verify that the synchronous 'order not found' responses do not leak order existence in a way that violates LSPS1 privacy expectations. No urgent security action is indicated by the commit itself.
Security signals we found
Removes asynchronous event-driven response path that could leave GetOrder requests unanswered if the user/LSP never calls back
Adds explicit 'order not found' error response for unknown order_id and unknown counterparty
Changes update_order_status to a pure state update, decoupling response generation from state mutation
No explicit mention of security, CVE, bug, vulnerability, or attacker in commit message or diff
Evidence from the diff
The patch refactors LSPS1 service handling of GetOrder requests. Previously handle_get_order_request emitted a CheckPaymentConfirmation event and required the caller to invoke update_order_status with a request_id to send a response. Now the service looks up the order in PeerState::outbound_channels_by_order_id and replies synchronously with the stored ChannelOrder. A new error code (101) is returned when the order or peer is unknown. update_order_status is retained but no longer sends a message; it only mutates local state. The integration test is updated to remove the event/callback dance. This is an API/behavior change rather than a vulnerability remediation.
Changed components
lightning-liquidity/src/lsps1/service.rslightning-liquidity/src/lsps1/peer_state.rslightning-liquidity/src/lsps1/event.rslightning-liquidity/src/lsps1/msgs.rslightning-liquidity/tests/lsps1_integration_tests.rsInspect captured patch +58 / −89
diff --git a/lightning-liquidity/src/lsps1/event.rs b/lightning-liquidity/src/lsps1/event.rs
index fdf3fc5..d966f8b 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 8402827..4f79a13 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 a3d2000..31ea41d 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<LSPS1ChannelInfo>,
- ) -> 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 9ce5e67..f4e1c1d 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,71 +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 lighting
- /// and then responds to client request.
- ///
- /// Should be called in response to receiving a [`LSPS1ServiceEvent::CheckPaymentConfirmation`] event.
///
- /// [`LSPS1ServiceEvent::CheckPaymentConfirmation`]: crate::lsps1::event::LSPS1ServiceEvent::CheckPaymentConfirmation
+ /// The LSP continously polls for checking payment confirmation on-chain or Lightning
+ /// and then responds to client request.
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<LSPS1ChannelInfo>,
) -> 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.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 {
@@ -364,7 +369,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,
@@ -376,7 +381,5 @@ fn is_valid(order: &LSPS1OrderParams, options: &LSPS1Options) -> bool {
1,
options.max_channel_expiry_blocks.into(),
order.channel_expiry_blocks.into(),
- );
-
- bool
+ )
}
diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs
index e799cce..ef210a3 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
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.