What changed, and why it matters
This commit shortens a rate-limiting cooldown in the LSPS5 webhook notification system from 10 seconds down to 100 milliseconds, and fixes a time-calculation bug that previously dropped sub-second precision. The change is described by the authors as a tuning fix to avoid multi-second delays for legitimate wake-up notifications while still throttling rapid reconnect churn. There is no direct evidence in the commit that this is a security vulnerability fix, but the affected code is a denial-of-service/amplification throttle, so any weakening of it has defensive-security relevance.
Review whether 100 ms provides adequate protection against rapid peer connect/disconnect churn and webhook URL amplification in production deployments. Consider adding explicit rate-limit tests for adversarial churn scenarios. Treat as a functional/DoS-hardening change rather than an urgent vulnerability patch unless further incident data emerges.
Security signals we found
Rate-limit/amplification throttle weakened (10s -> 100ms)
Time/duration arithmetic precision fix
Webhook notification cooldown reset logic changed
No explicit security framing in commit message or diff
Evidence from the diff
The patch modifies three files in the lightning-liquidity crate. In lsps0/ser.rs it replaces a whole-second duration_since implementation with one that preserves millisecond precision via signed_duration_since(...).to_std(). In lsps5/service.rs it lowers NOTIFICATION_COOLDOWN_RESET_INTERVAL from Duration::from_secs(10) to Duration::from_millis(100) and updates unit tests to use millisecond-scale timestamps. In lsps5_integration_tests.rs it adds a millisecond time-advance helper and changes the integration test to advance 100 ms instead of 11 seconds. The functional effect is that peer lifecycle events can clear webhook notification cooldowns much sooner.
Changed components
lightning-liquidity/src/lsps0/ser.rslightning-liquidity/src/lsps5/service.rslightning-liquidity/tests/lsps5_integration_tests.rsInspect captured patch +18 / −11
diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs
index 1ac900b..bbd3100 100644
--- a/lightning-liquidity/src/lsps0/ser.rs
+++ b/lightning-liquidity/src/lsps0/ser.rs
@@ -258,12 +258,7 @@ impl LSPSDateTime {
/// Returns the elapsed duration from `other` to `self`, or zero if `other` is later.
pub fn duration_since(&self, other: &Self) -> Duration {
- let diff_secs = self.0.timestamp().saturating_sub(other.0.timestamp());
- if diff_secs <= 0 {
- Duration::ZERO
- } else {
- Duration::from_secs(diff_secs as u64)
- }
+ self.0.signed_duration_since(other.0).to_std().unwrap_or(Duration::ZERO)
}
/// Returns the time in seconds since the unix epoch.
@@ -1007,8 +1002,11 @@ mod tests {
fn datetime_duration_since_is_directional() {
let earlier = LSPSDateTime::new_from_duration_since_epoch(Duration::from_secs(30));
let later = LSPSDateTime::new_from_duration_since_epoch(Duration::from_secs(90));
+ let later_with_millis =
+ LSPSDateTime::new_from_duration_since_epoch(Duration::from_millis(90_100));
assert_eq!(later.duration_since(&earlier), Duration::from_secs(60));
+ assert_eq!(later_with_millis.duration_since(&later), Duration::from_millis(100));
assert_eq!(earlier.duration_since(&later), Duration::ZERO);
}
diff --git a/lightning-liquidity/src/lsps5/service.rs b/lightning-liquidity/src/lsps5/service.rs
index acc77ef..babed1c 100644
--- a/lightning-liquidity/src/lsps5/service.rs
+++ b/lightning-liquidity/src/lsps5/service.rs
@@ -90,7 +90,7 @@ pub const NOTIFICATION_COOLDOWN_TIME: Duration = Duration::from_secs(60); // 1 m
/// This is distinct from [`NOTIFICATION_COOLDOWN_TIME`]: that cooldown protects the client from
/// repeated spammy wake-ups, while this reset throttle protects registered notification URLs from
/// amplification via rapid peer connect/disconnect churn.
-const NOTIFICATION_COOLDOWN_RESET_INTERVAL: Duration = Duration::from_secs(10);
+const NOTIFICATION_COOLDOWN_RESET_INTERVAL: Duration = Duration::from_millis(100);
// Default configuration for LSPS5 service.
impl Default for LSPS5ServiceConfig {
@@ -878,6 +878,10 @@ mod tests {
LSPSDateTime::new_from_duration_since_epoch(Duration::from_secs(seconds))
}
+ fn lsps_datetime_millis(milliseconds: u64) -> LSPSDateTime {
+ LSPSDateTime::new_from_duration_since_epoch(Duration::from_millis(milliseconds))
+ }
+
fn test_webhook(last_notification_sent: Option<LSPSDateTime>) -> (LSPS5AppName, Webhook) {
let app_name = LSPS5AppName::new("test_app".to_string()).unwrap();
let url = LSPS5WebhookUrl::new("https://example.com/webhook".to_string()).unwrap();
@@ -913,8 +917,8 @@ mod tests {
assert!(peer_state.needs_persist);
peer_state.needs_persist = false;
- let skipped_reset = lsps_datetime(2_009);
- let recent_notification = lsps_datetime(2_009);
+ let skipped_reset = lsps_datetime_millis(2_000_099);
+ let recent_notification = skipped_reset;
peer_state.webhooks_mut()[0].1.last_notification_sent = Some(recent_notification);
peer_state.needs_persist = false;
@@ -923,7 +927,7 @@ mod tests {
assert_eq!(peer_state.last_notification_cooldown_reset, Some(first_reset));
assert!(!peer_state.needs_persist);
- let allowed_reset = lsps_datetime(2_010);
+ let allowed_reset = lsps_datetime_millis(2_000_100);
peer_state.reset_notification_cooldown(allowed_reset);
assert_eq!(peer_state.webhooks()[0].1.last_notification_sent, None);
assert_eq!(peer_state.last_notification_cooldown_reset, Some(allowed_reset));
diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs
index 7a77b97..07e0351 100644
--- a/lightning-liquidity/tests/lsps5_integration_tests.rs
+++ b/lightning-liquidity/tests/lsps5_integration_tests.rs
@@ -274,6 +274,11 @@ impl MockTimeProvider {
let mut time = self.current_time.write().unwrap();
*time += Duration::from_secs(seconds);
}
+
+ fn advance_time_millis(&self, milliseconds: u64) {
+ let mut time = self.current_time.write().unwrap();
+ *time += Duration::from_millis(milliseconds);
+ }
}
impl TimeProvider for MockTimeProvider {
@@ -1409,7 +1414,7 @@ fn test_notifications_and_peer_connected_reset_is_throttled() {
);
// 7. Once the reset throttle has elapsed, peer_connected can reset the cooldown again.
- mock_time_provider.advance_time(11);
+ mock_time_provider.advance_time_millis(100);
service_node.liquidity_manager.peer_connected(client_node_id, &init_msg, false).unwrap();
let _ = service_handler.notify_payment_incoming(client_node_id);
let event = service_node.liquidity_manager.next_event().unwrap();
Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.