@@ -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,18 @@ struct StoredWebhook {
6064pub 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 ,
67+ }
68+
69+ /// Default maximum number of webhooks allowed per client.
70+ pub const DEFAULT_MAX_WEBHOOKS_PER_CLIENT : u32 = 10 ;
71+ /// Default notification cooldown time in hours.
72+ pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS : Duration = Duration :: from_secs ( 60 * 60 ) ; // 1 hour
73+
74+ // Default configuration for LSPS5 service.
75+ impl Default for LSPS5ServiceConfig {
76+ fn default ( ) -> Self {
77+ Self { max_webhooks_per_client : DEFAULT_MAX_WEBHOOKS_PER_CLIENT }
78+ }
6579}
6680
6781/// Service-side handler for the [`bLIP-55 / LSPS5`] webhook registration protocol.
@@ -78,8 +92,6 @@ pub struct LSPS5ServiceConfig {
7892/// - `lsps5.remove_webhook` -> delete a named webhook or return [`app_name_not_found`] error.
7993/// - Prune stale webhooks after a client has no open channels and no activity for at least
8094/// [`MIN_WEBHOOK_RETENTION_DAYS`].
81- /// - Rate-limit repeat notifications of the same method to a client by
82- /// [`notification_cooldown_hours`].
8395/// - Sign and enqueue outgoing webhook notifications:
8496/// - Construct JSON-RPC 2.0 Notification objects [`WebhookNotification`],
8597/// - Timestamp and LN-style zbase32-sign each payload,
@@ -94,7 +106,6 @@ pub struct LSPS5ServiceConfig {
94106/// [`bLIP-55 / LSPS5`]: https://github.com/lightning/blips/pull/55/files
95107/// [`max_webhooks_per_client`]: super::service::LSPS5ServiceConfig::max_webhooks_per_client
96108/// [`app_name_not_found`]: super::msgs::LSPS5ProtocolError::AppNameNotFound
97- /// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
98109/// [`WebhookNotification`]: super::msgs::WebhookNotification
99110/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
100111/// [`app_name`]: super::msgs::LSPS5AppName
@@ -318,10 +329,14 @@ where
318329 /// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your
319330 /// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client.
320331 ///
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+ ///
321335 /// # Parameters
322336 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
323337 ///
324338 /// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming
339+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
325340 pub fn notify_payment_incoming ( & self , client_id : PublicKey ) -> Result < ( ) , LSPS5ProtocolError > {
326341 let notification = WebhookNotification :: payment_incoming ( ) ;
327342 self . send_notifications_to_client_webhooks ( client_id, notification)
@@ -335,11 +350,15 @@ where
335350 /// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's
336351 /// registered webhooks.
337352 ///
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+ ///
338356 /// # Parameters
339357 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
340358 /// - `timeout`: the block height at which the channel contract will expire.
341359 ///
342360 /// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon
361+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
343362 pub fn notify_expiry_soon (
344363 & self , client_id : PublicKey , timeout : u32 ,
345364 ) -> Result < ( ) , LSPS5ProtocolError > {
@@ -353,10 +372,14 @@ where
353372 /// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification,
354373 /// signs it, and sends it to all of the client's registered webhook URLs.
355374 ///
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+ ///
356378 /// # Parameters
357379 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
358380 ///
359381 /// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest
382+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
360383 pub fn notify_liquidity_management_request (
361384 & self , client_id : PublicKey ,
362385 ) -> Result < ( ) , LSPS5ProtocolError > {
@@ -370,10 +393,14 @@ where
370393 /// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]
371394 /// notification, signs it, and enqueues HTTP POSTs to each registered webhook.
372395 ///
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+ ///
373399 /// # Parameters
374400 /// - `client_id`: the client's node-ID whose webhooks should be invoked.
375401 ///
376402 /// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming
403+ /// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS
377404 pub fn notify_onion_message_incoming (
378405 & self , client_id : PublicKey ,
379406 ) -> Result < ( ) , LSPS5ProtocolError > {
@@ -394,24 +421,34 @@ where
394421 let now =
395422 LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
396423
397- for ( app_name, webhook) in client_webhooks. iter_mut ( ) {
398- if webhook
399- . last_notification_sent
400- . get ( & notification. method )
401- . map ( |last_sent| now. clone ( ) . abs_diff ( & last_sent) )
402- . map_or ( true , |duration| {
403- duration >= self . config . notification_cooldown_hours . as_secs ( )
404- } ) {
405- webhook. last_notification_sent . insert ( notification. method . clone ( ) , now. clone ( ) ) ;
406- webhook. last_used = now. clone ( ) ;
407- self . send_notification (
408- client_id,
409- app_name. clone ( ) ,
410- webhook. url . clone ( ) ,
411- notification. clone ( ) ,
412- ) ?;
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 ) ;
413439 }
414440 }
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+ }
415452 Ok ( ( ) )
416453 }
417454
@@ -487,6 +524,15 @@ where
487524 . iter ( )
488525 . any ( |c| c. is_usable && c. counterparty . node_id == * client_id)
489526 }
527+
528+ pub ( crate ) fn peer_connected ( & self , counterparty_node_id : & PublicKey ) {
529+ let mut webhooks = self . webhooks . lock ( ) . unwrap ( ) ;
530+ if let Some ( client_webhooks) = webhooks. get_mut ( counterparty_node_id) {
531+ for webhook in client_webhooks. values_mut ( ) {
532+ webhook. last_notification_sent . clear ( ) ;
533+ }
534+ }
535+ }
490536}
491537
492538impl < CM : Deref , NS : Deref , TP : Deref > LSPSProtocolMessageHandler for LSPS5ServiceHandler < CM , NS , TP >
0 commit comments