Skip to content

Commit bae3224

Browse files
introduce SLOW_DOWN error so we don't silently fail to send an LSPS5/notification
1 parent 89962a1 commit bae3224

File tree

3 files changed

+63
-18
lines changed

3 files changed

+63
-18
lines changed

lightning-liquidity/src/lsps5/msgs.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ pub const LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE: i32 = 1010;
5151
pub const LSPS5_UNKNOWN_ERROR_CODE: i32 = 1000;
5252
/// An error occurred during serialization of LSPS5 webhook notification.
5353
pub const LSPS5_SERIALIZATION_ERROR_CODE: i32 = 1001;
54+
/// A notification was sent too frequently.
55+
pub const LSPS5_SLOW_DOWN_ERROR_CODE: i32 = 1002;
5456

5557
pub(crate) const LSPS5_SET_WEBHOOK_METHOD_NAME: &str = "lsps5.set_webhook";
5658
pub(crate) const LSPS5_LIST_WEBHOOKS_METHOD_NAME: &str = "lsps5.list_webhooks";
@@ -103,10 +105,18 @@ pub enum LSPS5ProtocolError {
103105

104106
/// Error during serialization of LSPS5 webhook notification.
105107
SerializationError,
108+
109+
/// A notification was sent too frequently.
110+
///
111+
/// This error indicates that the LSP is sending notifications
112+
/// too quickly, violating the notification cooldown [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]
113+
///
114+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
115+
SlowDownError,
106116
}
107117

108118
impl LSPS5ProtocolError {
109-
/// private code range so we never collide with the spec's codes
119+
/// The error code for the LSPS5 protocol error.
110120
pub fn code(&self) -> i32 {
111121
match self {
112122
LSPS5ProtocolError::AppNameTooLong | LSPS5ProtocolError::WebhookUrlTooLong => {
@@ -118,6 +128,7 @@ impl LSPS5ProtocolError {
118128
LSPS5ProtocolError::AppNameNotFound => LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE,
119129
LSPS5ProtocolError::UnknownError => LSPS5_UNKNOWN_ERROR_CODE,
120130
LSPS5ProtocolError::SerializationError => LSPS5_SERIALIZATION_ERROR_CODE,
131+
LSPS5ProtocolError::SlowDownError => LSPS5_SLOW_DOWN_ERROR_CODE,
121132
}
122133
}
123134
/// The error message for the LSPS5 protocol error.
@@ -133,6 +144,7 @@ impl LSPS5ProtocolError {
133144
LSPS5ProtocolError::SerializationError => {
134145
"Error serializing LSPS5 webhook notification"
135146
},
147+
LSPS5ProtocolError::SlowDownError => "Notification sent too frequently",
136148
}
137149
}
138150
}

lightning-liquidity/src/lsps5/service.rs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,14 @@ where
329329
/// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your
330330
/// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client.
331331
///
332+
/// This may fail if a similar notification was sent too recently,
333+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
334+
///
332335
/// # Parameters
333336
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
334337
///
335338
/// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming
339+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
336340
pub fn notify_payment_incoming(&self, client_id: PublicKey) -> Result<(), LSPS5ProtocolError> {
337341
let notification = WebhookNotification::payment_incoming();
338342
self.send_notifications_to_client_webhooks(client_id, notification)
@@ -346,11 +350,15 @@ where
346350
/// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's
347351
/// registered webhooks.
348352
///
353+
/// This may fail if a similar notification was sent too recently,
354+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
355+
///
349356
/// # Parameters
350357
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
351358
/// - `timeout`: the block height at which the channel contract will expire.
352359
///
353360
/// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon
361+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
354362
pub fn notify_expiry_soon(
355363
&self, client_id: PublicKey, timeout: u32,
356364
) -> Result<(), LSPS5ProtocolError> {
@@ -364,10 +372,14 @@ where
364372
/// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification,
365373
/// signs it, and sends it to all of the client's registered webhook URLs.
366374
///
375+
/// This may fail if a similar notification was sent too recently,
376+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
377+
///
367378
/// # Parameters
368379
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
369380
///
370381
/// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest
382+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
371383
pub fn notify_liquidity_management_request(
372384
&self, client_id: PublicKey,
373385
) -> Result<(), LSPS5ProtocolError> {
@@ -381,10 +393,14 @@ where
381393
/// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]
382394
/// notification, signs it, and enqueues HTTP POSTs to each registered webhook.
383395
///
396+
/// This may fail if a similar notification was sent too recently,
397+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
398+
///
384399
/// # Parameters
385400
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
386401
///
387402
/// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming
403+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
388404
pub fn notify_onion_message_incoming(
389405
&self, client_id: PublicKey,
390406
) -> Result<(), LSPS5ProtocolError> {
@@ -405,23 +421,34 @@ where
405421
let now =
406422
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch());
407423

408-
for (app_name, webhook) in client_webhooks.iter_mut() {
409-
if webhook
410-
.last_notification_sent
411-
.get(&notification.method)
412-
.map(|last_sent| now.clone().abs_diff(&last_sent))
413-
.map_or(true, |duration| duration >= DEFAULT_NOTIFICATION_COOLDOWN_HOURS.as_secs())
414-
{
415-
webhook.last_notification_sent.insert(notification.method.clone(), now.clone());
416-
webhook.last_used = now.clone();
417-
self.send_notification(
418-
client_id,
419-
app_name.clone(),
420-
webhook.url.clone(),
421-
notification.clone(),
422-
)?;
424+
// We must avoid sending multiple notifications of the same method
425+
// (other than lsps5.webhook_registered) close in time.
426+
if notification.method != WebhookNotificationMethod::LSPS5WebhookRegistered {
427+
let rate_limit_applies = client_webhooks.iter().any(|(_, webhook)| {
428+
webhook
429+
.last_notification_sent
430+
.get(&notification.method)
431+
.map(|last_sent| now.abs_diff(&last_sent))
432+
.map_or(false, |duration| {
433+
duration < DEFAULT_NOTIFICATION_COOLDOWN_HOURS.as_secs()
434+
})
435+
});
436+
437+
if rate_limit_applies {
438+
return Err(LSPS5ProtocolError::SlowDownError);
423439
}
424440
}
441+
442+
for (app_name, webhook) in client_webhooks.iter_mut() {
443+
webhook.last_notification_sent.insert(notification.method.clone(), now.clone());
444+
webhook.last_used = now.clone();
445+
self.send_notification(
446+
client_id,
447+
app_name.clone(),
448+
webhook.url.clone(),
449+
notification.clone(),
450+
)?;
451+
}
425452
Ok(())
426453
}
427454

lightning-liquidity/tests/lsps5_integration_tests.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,9 @@ fn test_send_notifications_and_peer_connected_resets_cooldown() {
10931093
}
10941094

10951095
// 2. Second notification before cooldown should NOT be sent
1096-
let _ = service_handler.notify_payment_incoming(client_node_id);
1096+
let result = service_handler.notify_payment_incoming(client_node_id);
1097+
let error = result.unwrap_err();
1098+
assert_eq!(error, LSPS5ProtocolError::SlowDownError);
10971099
assert!(
10981100
service_node.liquidity_manager.next_event().is_none(),
10991101
"Should not emit event due to cooldown"
@@ -1132,7 +1134,11 @@ fn test_send_notifications_and_peer_connected_resets_cooldown() {
11321134
}
11331135

11341136
// 5. Can't send payment_incoming notification again immediately after cooldown
1135-
let _ = service_handler.notify_payment_incoming(client_node_id);
1137+
let result = service_handler.notify_payment_incoming(client_node_id);
1138+
1139+
let error = result.unwrap_err();
1140+
assert_eq!(error, LSPS5ProtocolError::SlowDownError);
1141+
11361142
assert!(
11371143
service_node.liquidity_manager.next_event().is_none(),
11381144
"Should not emit event due to cooldown"

0 commit comments

Comments
 (0)