Skip to content

Commit bf87832

Browse files
authored
Merge pull request #3993 from martinsaposnic/lsps5-dos
Add LSPS5 DOS protections.
2 parents ae9da63 + 4370cff commit bf87832

File tree

7 files changed

+345
-4
lines changed

7 files changed

+345
-4
lines changed

lightning-liquidity/src/lsps1/service.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,20 @@ where
174174
&self.config
175175
}
176176

177+
/// Returns whether the peer currently has any active LSPS1 order flows.
178+
///
179+
/// An order is considered active only after we have validated the client's
180+
/// `CreateOrder` request and replied with a `CreateOrder` response containing
181+
/// an `order_id`.
182+
/// Pending requests that are still awaiting our response are deliberately NOT counted.
183+
pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool {
184+
let outer_state_lock = self.per_peer_state.read().unwrap();
185+
outer_state_lock.get(counterparty_node_id).map_or(false, |inner| {
186+
let peer_state = inner.lock().unwrap();
187+
!peer_state.outbound_channels_by_order_id.is_empty()
188+
})
189+
}
190+
177191
fn handle_get_info_request(
178192
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
179193
) -> Result<(), LightningError> {

lightning-liquidity/src/lsps2/service.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,15 @@ where
566566
&self.config
567567
}
568568

569+
/// Returns whether the peer has any active LSPS2 requests.
570+
pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool {
571+
let outer_state_lock = self.per_peer_state.read().unwrap();
572+
outer_state_lock.get(counterparty_node_id).map_or(false, |inner| {
573+
let peer_state = inner.lock().unwrap();
574+
!peer_state.outbound_channels_by_intercept_scid.is_empty()
575+
})
576+
}
577+
569578
/// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid.
570579
///
571580
/// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event.

lightning-liquidity/src/lsps5/client.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ where
185185
/// Also ensure the URL is valid, has HTTPS protocol, its length does not exceed [`MAX_WEBHOOK_URL_LENGTH`]
186186
/// and that the URL points to a public host.
187187
///
188+
/// Your request may fail if you recently opened a channel or started an LSPS1 / LSPS2 flow.
189+
/// Please retry shortly.
190+
///
188191
/// [`MAX_WEBHOOK_URL_LENGTH`]: super::msgs::MAX_WEBHOOK_URL_LENGTH
189192
/// [`MAX_APP_NAME_LENGTH`]: super::msgs::MAX_APP_NAME_LENGTH
190193
/// [`WebhookRegistered`]: super::event::LSPS5ClientEvent::WebhookRegistered

lightning-liquidity/src/lsps5/msgs.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub const LSPS5_UNKNOWN_ERROR_CODE: i32 = 1000;
5353
pub const LSPS5_SERIALIZATION_ERROR_CODE: i32 = 1001;
5454
/// A notification was sent too frequently.
5555
pub const LSPS5_SLOW_DOWN_ERROR_CODE: i32 = 1002;
56+
/// 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
57+
pub const LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE: i32 = 1003;
5658

5759
pub(crate) const LSPS5_SET_WEBHOOK_METHOD_NAME: &str = "lsps5.set_webhook";
5860
pub(crate) const LSPS5_LIST_WEBHOOKS_METHOD_NAME: &str = "lsps5.list_webhooks";
@@ -113,6 +115,10 @@ pub enum LSPS5ProtocolError {
113115
///
114116
/// [`NOTIFICATION_COOLDOWN_TIME`]: super::service::NOTIFICATION_COOLDOWN_TIME
115117
SlowDownError,
118+
119+
/// 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
120+
/// or initiate an LSPS1/LSPS2 interaction before retrying.
121+
NoPriorActivityError,
116122
}
117123

118124
impl LSPS5ProtocolError {
@@ -129,6 +135,7 @@ impl LSPS5ProtocolError {
129135
LSPS5ProtocolError::UnknownError => LSPS5_UNKNOWN_ERROR_CODE,
130136
LSPS5ProtocolError::SerializationError => LSPS5_SERIALIZATION_ERROR_CODE,
131137
LSPS5ProtocolError::SlowDownError => LSPS5_SLOW_DOWN_ERROR_CODE,
138+
LSPS5ProtocolError::NoPriorActivityError => LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE,
132139
}
133140
}
134141
/// The error message for the LSPS5 protocol error.
@@ -145,6 +152,9 @@ impl LSPS5ProtocolError {
145152
"Error serializing LSPS5 webhook notification"
146153
},
147154
LSPS5ProtocolError::SlowDownError => "Notification sent too frequently",
155+
LSPS5ProtocolError::NoPriorActivityError => {
156+
"Request rejected due to no prior activity with the LSP"
157+
},
148158
}
149159
}
150160
}
@@ -249,6 +259,9 @@ impl From<LSPSResponseError> for LSPS5ProtocolError {
249259
LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE => LSPS5ProtocolError::UnsupportedProtocol,
250260
LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE => LSPS5ProtocolError::TooManyWebhooks,
251261
LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE => LSPS5ProtocolError::AppNameNotFound,
262+
LSPS5_SERIALIZATION_ERROR_CODE => LSPS5ProtocolError::SerializationError,
263+
LSPS5_SLOW_DOWN_ERROR_CODE => LSPS5ProtocolError::SlowDownError,
264+
LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE => LSPS5ProtocolError::NoPriorActivityError,
252265
_ => LSPS5ProtocolError::UnknownError,
253266
}
254267
}
@@ -640,6 +653,12 @@ pub enum LSPS5Request {
640653
RemoveWebhook(RemoveWebhookRequest),
641654
}
642655

656+
impl LSPS5Request {
657+
pub(crate) fn is_state_allocating(&self) -> bool {
658+
matches!(self, LSPS5Request::SetWebhook(_))
659+
}
660+
}
661+
643662
/// An LSPS5 protocol response.
644663
#[derive(Clone, Debug, PartialEq, Eq)]
645664
pub enum LSPS5Response {

lightning-liquidity/src/lsps5/service.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ where
149149
}
150150
}
151151

152+
/// Enforces the prior-activity requirement for state-allocating LSPS5 requests (e.g.
153+
/// `lsps5.set_webhook`), rejecting and replying with `NoPriorActivityError` if not met.
154+
pub(crate) fn enforce_prior_activity_or_reject(
155+
&self, client_id: &PublicKey, lsps2_has_active_requests: bool, lsps1_has_activity: bool,
156+
request_id: LSPSRequestId,
157+
) -> Result<(), LightningError> {
158+
let can_accept = self.client_has_open_channel(client_id)
159+
|| lsps2_has_active_requests
160+
|| lsps1_has_activity;
161+
162+
let mut message_queue_notifier = self.pending_messages.notifier();
163+
if !can_accept {
164+
let error = LSPS5ProtocolError::NoPriorActivityError;
165+
let msg = LSPS5Message::Response(
166+
request_id,
167+
LSPS5Response::SetWebhookError(error.clone().into()),
168+
)
169+
.into();
170+
message_queue_notifier.enqueue(&client_id, msg);
171+
return Err(LightningError {
172+
err: error.message().into(),
173+
action: ErrorAction::IgnoreAndLog(Level::Info),
174+
});
175+
} else {
176+
Ok(())
177+
}
178+
}
179+
152180
fn check_prune_stale_webhooks<'a>(
153181
&self, outer_state_lock: &mut RwLockWriteGuard<'a, HashMap<PublicKey, PeerState>>,
154182
) {

lightning-liquidity/src/manager.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,29 @@ where
568568
LSPSMessage::LSPS5(msg @ LSPS5Message::Request(..)) => {
569569
match &self.lsps5_service_handler {
570570
Some(lsps5_service_handler) => {
571+
if let LSPS5Message::Request(ref req_id, ref req) = msg {
572+
if req.is_state_allocating() {
573+
let lsps2_has_active_requests = self
574+
.lsps2_service_handler
575+
.as_ref()
576+
.map_or(false, |h| h.has_active_requests(sender_node_id));
577+
#[cfg(lsps1_service)]
578+
let lsps1_has_active_requests = self
579+
.lsps1_service_handler
580+
.as_ref()
581+
.map_or(false, |h| h.has_active_requests(sender_node_id));
582+
#[cfg(not(lsps1_service))]
583+
let lsps1_has_active_requests = false;
584+
585+
lsps5_service_handler.enforce_prior_activity_or_reject(
586+
sender_node_id,
587+
lsps2_has_active_requests,
588+
lsps1_has_active_requests,
589+
req_id.clone(),
590+
)?
591+
}
592+
}
593+
571594
lsps5_service_handler.handle_message(msg, sender_node_id)?;
572595
},
573596
None => {

0 commit comments

Comments
 (0)