-
Notifications
You must be signed in to change notification settings - Fork 418
LSPS5: Correct notification cooldown & reset logic #3975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,7 +51,11 @@ struct StoredWebhook { | |
_app_name: LSPS5AppName, | ||
url: LSPS5WebhookUrl, | ||
_counterparty_node_id: PublicKey, | ||
// Timestamp used for tracking when the webhook was created / updated, or when the last notification was sent. | ||
// This is used to determine if the webhook is stale and should be pruned. | ||
last_used: LSPSDateTime, | ||
// Map of last notification sent timestamps for each notification method. | ||
// This is used to enforce notification cooldowns. | ||
last_notification_sent: HashMap<WebhookNotificationMethod, LSPSDateTime>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EEk, we end up with a Secondly, indexing this by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, probably gonna extract that into a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagine the last level can simply be removed, there's no reason to track per-webhook last-sent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is described in the spec this way, but I guess there's nothing keeping us from applying a more coarse grained approach in our implementation. Now added a corresponding change to the persistence PR coming up. |
||
} | ||
|
||
|
@@ -60,8 +64,18 @@ struct StoredWebhook { | |
pub struct LSPS5ServiceConfig { | ||
/// Maximum number of webhooks allowed per client. | ||
pub max_webhooks_per_client: u32, | ||
/// Minimum time between sending the same notification type in hours (default: 24) | ||
pub notification_cooldown_hours: Duration, | ||
} | ||
|
||
/// Default maximum number of webhooks allowed per client. | ||
pub const DEFAULT_MAX_WEBHOOKS_PER_CLIENT: u32 = 10; | ||
/// Default notification cooldown time in hours. | ||
pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS: Duration = Duration::from_secs(60 * 60); // 1 hour | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still seems wayyyy too high. If we get two payments an hour apart, just because the first wakeup was missed doesn't mean we don't want to send another. A phone may be disconnected from the network or low on battery when the first was sent and the phone decided not to do a wakeup, but it may well be in a different state an hour later. Yea, we don't want to send 1000 notifications in an hour, but we definitely should be comfortable sending five in an hour. We either need to crank this way down (one a minute?) or track and allow bursts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I tend to agree. We might want to amend the spec first though, as it currently states:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lightning/blips#64 spec change PR |
||
|
||
// Default configuration for LSPS5 service. | ||
impl Default for LSPS5ServiceConfig { | ||
fn default() -> Self { | ||
Self { max_webhooks_per_client: DEFAULT_MAX_WEBHOOKS_PER_CLIENT } | ||
} | ||
} | ||
|
||
/// Service-side handler for the [`bLIP-55 / LSPS5`] webhook registration protocol. | ||
|
@@ -78,8 +92,6 @@ pub struct LSPS5ServiceConfig { | |
/// - `lsps5.remove_webhook` -> delete a named webhook or return [`app_name_not_found`] error. | ||
/// - Prune stale webhooks after a client has no open channels and no activity for at least | ||
/// [`MIN_WEBHOOK_RETENTION_DAYS`]. | ||
/// - Rate-limit repeat notifications of the same method to a client by | ||
/// [`notification_cooldown_hours`]. | ||
/// - Sign and enqueue outgoing webhook notifications: | ||
/// - Construct JSON-RPC 2.0 Notification objects [`WebhookNotification`], | ||
/// - Timestamp and LN-style zbase32-sign each payload, | ||
|
@@ -94,7 +106,6 @@ pub struct LSPS5ServiceConfig { | |
/// [`bLIP-55 / LSPS5`]: https://github.com/lightning/blips/pull/55/files | ||
/// [`max_webhooks_per_client`]: super::service::LSPS5ServiceConfig::max_webhooks_per_client | ||
/// [`app_name_not_found`]: super::msgs::LSPS5ProtocolError::AppNameNotFound | ||
/// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours | ||
/// [`WebhookNotification`]: super::msgs::WebhookNotification | ||
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification | ||
/// [`app_name`]: super::msgs::LSPS5AppName | ||
|
@@ -318,10 +329,14 @@ where | |
/// This builds a [`WebhookNotificationMethod::LSPS5PaymentIncoming`] webhook notification, signs it with your | ||
/// node key, and enqueues HTTP POSTs to all registered webhook URLs for that client. | ||
/// | ||
/// This may fail if a similar notification was sent too recently, | ||
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All these links should be to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this PR changed it so it's not a config anymore. it does not make sense that it's called "default" though, I will change that |
||
/// | ||
/// # Parameters | ||
/// - `client_id`: the client's node-ID whose webhooks should be invoked. | ||
/// | ||
/// [`WebhookNotificationMethod::LSPS5PaymentIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5PaymentIncoming | ||
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS | ||
pub fn notify_payment_incoming(&self, client_id: PublicKey) -> Result<(), LSPS5ProtocolError> { | ||
let notification = WebhookNotification::payment_incoming(); | ||
self.send_notifications_to_client_webhooks(client_id, notification) | ||
|
@@ -335,11 +350,15 @@ where | |
/// the `timeout` block height, signs it, and enqueues HTTP POSTs to the client's | ||
/// registered webhooks. | ||
/// | ||
/// This may fail if a similar notification was sent too recently, | ||
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]. | ||
/// | ||
/// # Parameters | ||
/// - `client_id`: the client's node-ID whose webhooks should be invoked. | ||
/// - `timeout`: the block height at which the channel contract will expire. | ||
/// | ||
/// [`WebhookNotificationMethod::LSPS5ExpirySoon`]: super::msgs::WebhookNotificationMethod::LSPS5ExpirySoon | ||
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS | ||
pub fn notify_expiry_soon( | ||
&self, client_id: PublicKey, timeout: u32, | ||
) -> Result<(), LSPS5ProtocolError> { | ||
|
@@ -353,10 +372,14 @@ where | |
/// liquidity for `client_id`. Builds a [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`] notification, | ||
/// signs it, and sends it to all of the client's registered webhook URLs. | ||
/// | ||
/// This may fail if a similar notification was sent too recently, | ||
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]. | ||
/// | ||
/// # Parameters | ||
/// - `client_id`: the client's node-ID whose webhooks should be invoked. | ||
/// | ||
/// [`WebhookNotificationMethod::LSPS5LiquidityManagementRequest`]: super::msgs::WebhookNotificationMethod::LSPS5LiquidityManagementRequest | ||
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS | ||
pub fn notify_liquidity_management_request( | ||
&self, client_id: PublicKey, | ||
) -> Result<(), LSPS5ProtocolError> { | ||
|
@@ -370,10 +393,14 @@ where | |
/// for `client_id` while the client is offline. Builds a [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`] | ||
/// notification, signs it, and enqueues HTTP POSTs to each registered webhook. | ||
/// | ||
/// This may fail if a similar notification was sent too recently, | ||
/// violating the notification cooldown period defined in [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]. | ||
/// | ||
/// # Parameters | ||
/// - `client_id`: the client's node-ID whose webhooks should be invoked. | ||
/// | ||
/// [`WebhookNotificationMethod::LSPS5OnionMessageIncoming`]: super::msgs::WebhookNotificationMethod::LSPS5OnionMessageIncoming | ||
/// [`DEFAULT_NOTIFICATION_COOLDOWN_HOURS`]: super::service::DEFAULT_NOTIFICATION_COOLDOWN_HOURS | ||
pub fn notify_onion_message_incoming( | ||
&self, client_id: PublicKey, | ||
) -> Result<(), LSPS5ProtocolError> { | ||
|
@@ -394,24 +421,34 @@ where | |
let now = | ||
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch()); | ||
|
||
for (app_name, webhook) in client_webhooks.iter_mut() { | ||
if webhook | ||
.last_notification_sent | ||
.get(¬ification.method) | ||
.map(|last_sent| now.clone().abs_diff(&last_sent)) | ||
.map_or(true, |duration| { | ||
duration >= self.config.notification_cooldown_hours.as_secs() | ||
}) { | ||
webhook.last_notification_sent.insert(notification.method.clone(), now.clone()); | ||
webhook.last_used = now.clone(); | ||
self.send_notification( | ||
client_id, | ||
app_name.clone(), | ||
webhook.url.clone(), | ||
notification.clone(), | ||
)?; | ||
// We must avoid sending multiple notifications of the same method | ||
// (other than lsps5.webhook_registered) close in time. | ||
if notification.method != WebhookNotificationMethod::LSPS5WebhookRegistered { | ||
let rate_limit_applies = client_webhooks.iter().any(|(_, webhook)| { | ||
webhook | ||
.last_notification_sent | ||
.get(¬ification.method) | ||
.map(|last_sent| now.abs_diff(&last_sent)) | ||
.map_or(false, |duration| { | ||
duration < DEFAULT_NOTIFICATION_COOLDOWN_HOURS.as_secs() | ||
}) | ||
}); | ||
|
||
if rate_limit_applies { | ||
return Err(LSPS5ProtocolError::SlowDownError); | ||
} | ||
} | ||
|
||
for (app_name, webhook) in client_webhooks.iter_mut() { | ||
webhook.last_notification_sent.insert(notification.method.clone(), now.clone()); | ||
webhook.last_used = now.clone(); | ||
self.send_notification( | ||
client_id, | ||
app_name.clone(), | ||
webhook.url.clone(), | ||
notification.clone(), | ||
)?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
|
@@ -487,6 +524,15 @@ where | |
.iter() | ||
.any(|c| c.is_usable && c.counterparty.node_id == *client_id) | ||
} | ||
|
||
pub(crate) fn peer_connected(&self, counterparty_node_id: &PublicKey) { | ||
let mut webhooks = self.webhooks.lock().unwrap(); | ||
if let Some(client_webhooks) = webhooks.get_mut(counterparty_node_id) { | ||
for webhook in client_webhooks.values_mut() { | ||
webhook.last_notification_sent.clear(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit orthogonal, but do we even need a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add comments explaining because it was even hard for me to remember the difference. they are different because
last_notification_sent is used for the notification_cooldown logic, and last_used is used for the stale_webhooks logic |
||
} | ||
} | ||
} | ||
} | ||
|
||
impl<CM: Deref, NS: Deref, TP: Deref> LSPSProtocolMessageHandler for LSPS5ServiceHandler<CM, NS, TP> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't get there in github, but it seems the above comment
is wrong given we match spec'd and non-spec'd codes here and elsewhere?