Skip to content

Commit a4a560b

Browse files
fixup: addressing comments, details below:
- notification_cooldown_hours is not configurable anymore - revert duration rename to last_sent so it's clearer - add comments on last_used and last_notification_sent to clarify its purpose - introduce SLOW_DOWN error so we don't silently fail to send a notification
1 parent 60cedd3 commit a4a560b

File tree

4 files changed

+71
-35
lines changed

4 files changed

+71
-35
lines changed

lightning-liquidity/src/lsps5/event.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ pub enum LSPS5ServiceEvent {
3131
/// The LSP should send an HTTP POST to the [`url`], using the
3232
/// JSON-serialized [`notification`] as the body and including the `headers`.
3333
/// If the HTTP request fails, the LSP may implement a retry policy according to its
34-
/// implementation preferences, but must respect rate-limiting as defined in
35-
/// [`notification_cooldown_hours`].
34+
/// implementation preferences.
3635
///
3736
/// The notification is signed using the LSP's node ID to ensure authenticity
3837
/// when received by the client. The client verifies this signature using
3938
/// [`validate`], which guards against replay attacks and tampering.
4039
///
4140
/// [`validate`]: super::validator::LSPS5Validator::validate
42-
/// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
4341
/// [`url`]: super::msgs::LSPS5WebhookUrl
4442
/// [`notification`]: super::msgs::WebhookNotification
4543
SendWebhookNotification {

lightning-liquidity/src/lsps5/msgs.rs

Lines changed: 12 additions & 0 deletions
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,6 +105,14 @@ 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 {
@@ -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: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ struct StoredWebhook {
5151
_app_name: LSPS5AppName,
5252
url: LSPS5WebhookUrl,
5353
_counterparty_node_id: PublicKey,
54+
// Timestamp used for tracking when the webhook was created / updated, or when the last notification was sent.
55+
// This is used to determine if the webhook is stale and should be pruned.
5456
last_used: LSPSDateTime,
57+
// Map of last notification sent timestamps for each notification method.
58+
// This is used to enforce notification cooldowns.
5559
last_notification_sent: HashMap<WebhookNotificationMethod, LSPSDateTime>,
5660
}
5761

@@ -60,8 +64,6 @@ struct StoredWebhook {
6064
pub struct LSPS5ServiceConfig {
6165
/// Maximum number of webhooks allowed per client.
6266
pub max_webhooks_per_client: u32,
63-
/// Minimum time between sending the same notification type in hours (default: 24)
64-
pub notification_cooldown_hours: Duration,
6567
}
6668

6769
/// Default maximum number of webhooks allowed per client.
@@ -72,10 +74,7 @@ pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS: Duration = Duration::from_secs(60
7274
// Default configuration for LSPS5 service.
7375
impl Default for LSPS5ServiceConfig {
7476
fn default() -> Self {
75-
Self {
76-
max_webhooks_per_client: DEFAULT_MAX_WEBHOOKS_PER_CLIENT,
77-
notification_cooldown_hours: DEFAULT_NOTIFICATION_COOLDOWN_HOURS,
78-
}
77+
Self { max_webhooks_per_client: DEFAULT_MAX_WEBHOOKS_PER_CLIENT }
7978
}
8079
}
8180

@@ -93,8 +92,6 @@ impl Default for LSPS5ServiceConfig {
9392
/// - `lsps5.remove_webhook` -> delete a named webhook or return [`app_name_not_found`] error.
9493
/// - Prune stale webhooks after a client has no open channels and no activity for at least
9594
/// [`MIN_WEBHOOK_RETENTION_DAYS`].
96-
/// - Rate-limit repeat notifications of the same method to a client by
97-
/// [`notification_cooldown_hours`].
9895
/// - Sign and enqueue outgoing webhook notifications:
9996
/// - Construct JSON-RPC 2.0 Notification objects [`WebhookNotification`],
10097
/// - Timestamp and LN-style zbase32-sign each payload,
@@ -109,7 +106,6 @@ impl Default for LSPS5ServiceConfig {
109106
/// [`bLIP-55 / LSPS5`]: https://github.com/lightning/blips/pull/55/files
110107
/// [`max_webhooks_per_client`]: super::service::LSPS5ServiceConfig::max_webhooks_per_client
111108
/// [`app_name_not_found`]: super::msgs::LSPS5ProtocolError::AppNameNotFound
112-
/// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
113109
/// [`WebhookNotification`]: super::msgs::WebhookNotification
114110
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
115111
/// [`app_name`]: super::msgs::LSPS5AppName
@@ -225,23 +221,27 @@ where
225221
}
226222

227223
if !no_change {
228-
self.send_webhook_registered_notification(
224+
let result = self.send_webhook_registered_notification(
229225
counterparty_node_id,
230226
params.app_name,
231227
params.webhook,
232-
)
233-
.map_err(|e| {
228+
);
229+
230+
// If the send_notification failed because of a SLOW_DOWN_ERROR, it means we sent this
231+
// notification recently, and the user has not seen it yet. It's safe to continue, but we still need to handle other error types.
232+
if result.is_err() && !matches!(result, Err(LSPS5ProtocolError::SlowDownError)) {
233+
let e = result.unwrap_err();
234234
let msg = LSPS5Message::Response(
235235
request_id.clone(),
236236
LSPS5Response::SetWebhookError(e.clone().into()),
237237
)
238238
.into();
239239
self.pending_messages.enqueue(&counterparty_node_id, msg);
240-
LightningError {
240+
return Err(LightningError {
241241
err: e.message().into(),
242242
action: ErrorAction::IgnoreAndLog(Level::Info),
243-
}
244-
})?;
243+
});
244+
}
245245
}
246246

247247
let msg = LSPS5Message::Response(
@@ -327,10 +327,14 @@ where
327327
/// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your
328328
/// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client.
329329
///
330+
/// This may fail if a similar notification was sent too recently,
331+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
332+
///
330333
/// # Parameters
331334
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
332335
///
333336
/// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming
337+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
334338
pub fn notify_payment_incoming(&self, client_id: PublicKey) -> Result<(), LSPS5ProtocolError> {
335339
let notification = WebhookNotification::payment_incoming();
336340
self.send_notifications_to_client_webhooks(client_id, notification)
@@ -344,11 +348,15 @@ where
344348
/// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's
345349
/// registered webhooks.
346350
///
351+
/// This may fail if a similar notification was sent too recently,
352+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
353+
///
347354
/// # Parameters
348355
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
349356
/// - `timeout`: the block height at which the channel contract will expire.
350357
///
351358
/// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon
359+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
352360
pub fn notify_expiry_soon(
353361
&self, client_id: PublicKey, timeout: u32,
354362
) -> Result<(), LSPS5ProtocolError> {
@@ -362,10 +370,14 @@ where
362370
/// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification,
363371
/// signs it, and sends it to all of the client's registered webhook URLs.
364372
///
373+
/// This may fail if a similar notification was sent too recently,
374+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
375+
///
365376
/// # Parameters
366377
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
367378
///
368379
/// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest
380+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
369381
pub fn notify_liquidity_management_request(
370382
&self, client_id: PublicKey,
371383
) -> Result<(), LSPS5ProtocolError> {
@@ -379,10 +391,14 @@ where
379391
/// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]
380392
/// notification, signs it, and enqueues HTTP POSTs to each registered webhook.
381393
///
394+
/// This may fail if a similar notification was sent too recently,
395+
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`].
396+
///
382397
/// # Parameters
383398
/// - `client_id`: the client's node-ID whose webhooks should be invoked.
384399
///
385400
/// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming
401+
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
386402
pub fn notify_onion_message_incoming(
387403
&self, client_id: PublicKey,
388404
) -> Result<(), LSPS5ProtocolError> {
@@ -403,23 +419,27 @@ where
403419
let now =
404420
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch());
405421

406-
for (app_name, webhook) in client_webhooks.iter_mut() {
407-
if webhook
422+
let rate_limit_applies = client_webhooks.iter().any(|(_, webhook)| {
423+
webhook
408424
.last_notification_sent
409425
.get(&notification.method)
410-
.map(|last_sent| now.clone().abs_diff(&last_sent))
411-
.map_or(true, |last_sent| {
412-
last_sent >= self.config.notification_cooldown_hours.as_secs()
413-
}) {
414-
webhook.last_notification_sent.insert(notification.method.clone(), now.clone());
415-
webhook.last_used = now.clone();
416-
self.send_notification(
417-
client_id,
418-
app_name.clone(),
419-
webhook.url.clone(),
420-
notification.clone(),
421-
)?;
422-
}
426+
.map(|last_sent| now.abs_diff(&last_sent))
427+
.map_or(false, |duration| duration < DEFAULT_NOTIFICATION_COOLDOWN_HOURS.as_secs())
428+
});
429+
430+
if rate_limit_applies {
431+
return Err(LSPS5ProtocolError::SlowDownError);
432+
}
433+
434+
for (app_name, webhook) in client_webhooks.iter_mut() {
435+
webhook.last_notification_sent.insert(notification.method.clone(), now.clone());
436+
webhook.last_used = now.clone();
437+
self.send_notification(
438+
client_id,
439+
app_name.clone(),
440+
webhook.url.clone(),
441+
notification.clone(),
442+
)?;
423443
}
424444
Ok(())
425445
}

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)