Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lightning-liquidity/src/lsps1/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@ where
&self.config
}

/// Returns whether the peer currently has any active LSPS1 order flows.
///
/// An order is considered active only after we have validated the client's
/// `CreateOrder` request and replied with a `CreateOrder` response containing
/// an `order_id`.
/// 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| {
let peer_state = inner.lock().unwrap();
!peer_state.outbound_channels_by_order_id.is_empty()
})
}

fn handle_get_info_request(
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
) -> Result<(), LightningError> {
Expand Down
9 changes: 9 additions & 0 deletions lightning-liquidity/src/lsps2/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,15 @@ where
&self.config
}

/// Returns whether the peer has any active LSPS2 requests.
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| {
let peer_state = inner.lock().unwrap();
!peer_state.outbound_channels_by_intercept_scid.is_empty()
})
}

/// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid.
///
/// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event.
Expand Down
3 changes: 3 additions & 0 deletions lightning-liquidity/src/lsps5/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ where
/// Also ensure the URL is valid, has HTTPS protocol, its length does not exceed [`MAX_WEBHOOK_URL_LENGTH`]
/// and that the URL points to a public host.
///
/// Your request may fail if you recently opened a channel or started an LSPS1 / LSPS2 flow.
/// Please retry shortly.
///
/// [`MAX_WEBHOOK_URL_LENGTH`]: super::msgs::MAX_WEBHOOK_URL_LENGTH
/// [`MAX_APP_NAME_LENGTH`]: super::msgs::MAX_APP_NAME_LENGTH
/// [`WebhookRegistered`]: super::event::LSPS5ClientEvent::WebhookRegistered
Expand Down
19 changes: 19 additions & 0 deletions lightning-liquidity/src/lsps5/msgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub const LSPS5_UNKNOWN_ERROR_CODE: i32 = 1000;
pub const LSPS5_SERIALIZATION_ERROR_CODE: i32 = 1001;
/// A notification was sent too frequently.
pub const LSPS5_SLOW_DOWN_ERROR_CODE: i32 = 1002;
/// A request was rejected because the client has no prior activity with the LSP (no open channel and no active LSPS1 or LSPS2 flow). The client should first open a channel
pub const LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE: i32 = 1003;

pub(crate) const LSPS5_SET_WEBHOOK_METHOD_NAME: &str = "lsps5.set_webhook";
pub(crate) const LSPS5_LIST_WEBHOOKS_METHOD_NAME: &str = "lsps5.list_webhooks";
Expand Down Expand Up @@ -113,6 +115,10 @@ pub enum LSPS5ProtocolError {
///
/// [`NOTIFICATION_COOLDOWN_TIME`]: super::service::NOTIFICATION_COOLDOWN_TIME
SlowDownError,

/// Request rejected because the client has no prior activity with the LSP (no open channel and no active LSPS1 or LSPS2 flow). The client should first open a channel
/// or initiate an LSPS1/LSPS2 interaction before retrying.
NoPriorActivityError,
}

impl LSPS5ProtocolError {
Expand All @@ -129,6 +135,7 @@ impl LSPS5ProtocolError {
LSPS5ProtocolError::UnknownError => LSPS5_UNKNOWN_ERROR_CODE,
LSPS5ProtocolError::SerializationError => LSPS5_SERIALIZATION_ERROR_CODE,
LSPS5ProtocolError::SlowDownError => LSPS5_SLOW_DOWN_ERROR_CODE,
LSPS5ProtocolError::NoPriorActivityError => LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE,
}
}
/// The error message for the LSPS5 protocol error.
Expand All @@ -145,6 +152,9 @@ impl LSPS5ProtocolError {
"Error serializing LSPS5 webhook notification"
},
LSPS5ProtocolError::SlowDownError => "Notification sent too frequently",
LSPS5ProtocolError::NoPriorActivityError => {
"Request rejected due to no prior activity with the LSP"
},
}
}
}
Expand Down Expand Up @@ -249,6 +259,9 @@ impl From<LSPSResponseError> for LSPS5ProtocolError {
LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE => LSPS5ProtocolError::UnsupportedProtocol,
LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE => LSPS5ProtocolError::TooManyWebhooks,
LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE => LSPS5ProtocolError::AppNameNotFound,
LSPS5_SERIALIZATION_ERROR_CODE => LSPS5ProtocolError::SerializationError,
LSPS5_SLOW_DOWN_ERROR_CODE => LSPS5ProtocolError::SlowDownError,
LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE => LSPS5ProtocolError::NoPriorActivityError,
_ => LSPS5ProtocolError::UnknownError,
}
}
Expand Down Expand Up @@ -640,6 +653,12 @@ pub enum LSPS5Request {
RemoveWebhook(RemoveWebhookRequest),
}

impl LSPS5Request {
pub(crate) fn is_state_allocating(&self) -> bool {
matches!(self, LSPS5Request::SetWebhook(_))
}
}

/// An LSPS5 protocol response.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LSPS5Response {
Expand Down
28 changes: 28 additions & 0 deletions lightning-liquidity/src/lsps5/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,34 @@ where
}
}

/// Enforces the prior-activity requirement for state-allocating LSPS5 requests (e.g.
/// `lsps5.set_webhook`), rejecting and replying with `NoPriorActivityError` if not met.
pub(crate) fn enforce_prior_activity_or_reject(
&self, client_id: &PublicKey, lsps2_has_active_requests: bool, lsps1_has_activity: bool,
request_id: LSPSRequestId,
) -> Result<(), LightningError> {
let can_accept = self.client_has_open_channel(client_id)
|| lsps2_has_active_requests
|| lsps1_has_activity;

let mut message_queue_notifier = self.pending_messages.notifier();
if !can_accept {
let error = LSPS5ProtocolError::NoPriorActivityError;
let msg = LSPS5Message::Response(
request_id,
LSPS5Response::SetWebhookError(error.clone().into()),
)
.into();
message_queue_notifier.enqueue(&client_id, msg);
return Err(LightningError {
err: error.message().into(),
action: ErrorAction::IgnoreAndLog(Level::Info),
});
} else {
Ok(())
}
}

fn check_prune_stale_webhooks<'a>(
&self, outer_state_lock: &mut RwLockWriteGuard<'a, HashMap<PublicKey, PeerState>>,
) {
Expand Down
23 changes: 23 additions & 0 deletions lightning-liquidity/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,29 @@ where
LSPSMessage::LSPS5(msg @ LSPS5Message::Request(..)) => {
match &self.lsps5_service_handler {
Some(lsps5_service_handler) => {
if let LSPS5Message::Request(ref req_id, ref req) = msg {
if req.is_state_allocating() {
let lsps2_has_active_requests = self
.lsps2_service_handler
.as_ref()
.map_or(false, |h| h.has_active_requests(sender_node_id));
#[cfg(lsps1_service)]
let lsps1_has_active_requests = self
.lsps1_service_handler
.as_ref()
.map_or(false, |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,
lsps1_has_active_requests,
req_id.clone(),
)?
}
}

lsps5_service_handler.handle_message(msg, sender_node_id)?;
},
None => {
Expand Down
Loading
Loading