@@ -83,18 +83,24 @@ impl Default for LSPS5ClientConfig {
8383 }
8484}
8585
86- struct PeerState {
86+ struct PeerState < TP : Deref >
87+ where
88+ TP :: Target : TimeProvider ,
89+ {
8790 pending_set_webhook_requests :
8891 HashMap < LSPSRequestId , ( LSPS5AppName , LSPS5WebhookUrl , LSPSDateTime ) > ,
8992 pending_list_webhooks_requests : HashMap < LSPSRequestId , LSPSDateTime > ,
9093 pending_remove_webhook_requests : HashMap < LSPSRequestId , ( LSPS5AppName , LSPSDateTime ) > ,
9194 last_cleanup : Option < LSPSDateTime > ,
9295 max_age_secs : Duration ,
93- time_provider : Arc < dyn TimeProvider > ,
96+ time_provider : TP ,
9497}
9598
96- impl PeerState {
97- fn new ( max_age_secs : Duration , time_provider : Arc < dyn TimeProvider > ) -> Self {
99+ impl < TP : Deref > PeerState < TP >
100+ where
101+ TP :: Target : TimeProvider ,
102+ {
103+ fn new ( max_age_secs : Duration , time_provider : TP ) -> Self {
98104 Self {
99105 pending_set_webhook_requests : new_hash_map ( ) ,
100106 pending_list_webhooks_requests : new_hash_map ( ) ,
@@ -109,27 +115,29 @@ impl PeerState {
109115 let now =
110116 LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
111117 // Only run cleanup once per minute to avoid excessive processing
112- let minute = 60 ;
118+ const CLEANUP_INTERVAL : Duration = Duration :: from_secs ( 60 ) ;
113119 if let Some ( last_cleanup) = & self . last_cleanup {
114- if now. abs_diff ( last_cleanup. clone ( ) ) < minute {
120+ let time_since_last_cleanup = Duration :: from_secs ( now. abs_diff ( last_cleanup. clone ( ) ) ) ;
121+ if time_since_last_cleanup < CLEANUP_INTERVAL {
115122 return ;
116123 }
117124 }
118125
119126 self . last_cleanup = Some ( now. clone ( ) ) ;
120127
121128 self . pending_set_webhook_requests . retain ( |_, ( _, _, timestamp) | {
122- timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( )
129+ Duration :: from_secs ( timestamp. abs_diff ( now. clone ( ) ) ) < self . max_age_secs
130+ } ) ;
131+ self . pending_list_webhooks_requests . retain ( |_, timestamp| {
132+ Duration :: from_secs ( timestamp. abs_diff ( now. clone ( ) ) ) < self . max_age_secs
123133 } ) ;
124- self . pending_list_webhooks_requests
125- . retain ( |_, timestamp| timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( ) ) ;
126134 self . pending_remove_webhook_requests . retain ( |_, ( _, timestamp) | {
127- timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( )
135+ Duration :: from_secs ( timestamp. abs_diff ( now. clone ( ) ) ) < self . max_age_secs
128136 } ) ;
129137 }
130138}
131139
132- /// Client‐ side handler for the LSPS5 (bLIP-55) webhook registration protocol.
140+ /// Client- side handler for the LSPS5 (bLIP-55) webhook registration protocol.
133141///
134142/// `LSPS5ClientHandler` is the primary interface for LSP clients
135143/// to register, list, and remove webhook endpoints with an LSP, and to parse
@@ -146,27 +154,29 @@ impl PeerState {
146154/// [`lsps5.set_webhook`]: super::msgs::LSPS5Request::SetWebhook
147155/// [`lsps5.list_webhooks`]: super::msgs::LSPS5Request::ListWebhooks
148156/// [`lsps5.remove_webhook`]: super::msgs::LSPS5Request::RemoveWebhook
149- pub struct LSPS5ClientHandler < ES : Deref >
157+ pub struct LSPS5ClientHandler < ES : Deref , TP : Deref + Clone >
150158where
151159 ES :: Target : EntropySource ,
160+ TP :: Target : TimeProvider ,
152161{
153162 pending_messages : Arc < MessageQueue > ,
154163 pending_events : Arc < EventQueue > ,
155164 entropy_source : ES ,
156- per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState > > > ,
165+ per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState < TP > > > > ,
157166 config : LSPS5ClientConfig ,
158- time_provider : Arc < dyn TimeProvider > ,
167+ time_provider : TP ,
159168 recent_signatures : Mutex < VecDeque < ( String , LSPSDateTime ) > > ,
160169}
161170
162- impl < ES : Deref > LSPS5ClientHandler < ES >
171+ impl < ES : Deref , TP : Deref + Clone > LSPS5ClientHandler < ES , TP >
163172where
164173 ES :: Target : EntropySource ,
174+ TP :: Target : TimeProvider ,
165175{
166176 /// Constructs an `LSPS5ClientHandler`.
167177 pub ( crate ) fn new (
168178 entropy_source : ES , pending_messages : Arc < MessageQueue > , pending_events : Arc < EventQueue > ,
169- config : LSPS5ClientConfig , time_provider : Arc < dyn TimeProvider > ,
179+ config : LSPS5ClientConfig , time_provider : TP ,
170180 ) -> Self {
171181 let max_signatures = config. signature_config . max_signatures . clone ( ) ;
172182 Self {
@@ -182,11 +192,11 @@ where
182192
183193 fn with_peer_state < F , R > ( & self , counterparty_node_id : PublicKey , f : F ) -> R
184194 where
185- F : FnOnce ( & mut PeerState ) -> R ,
195+ F : FnOnce ( & mut PeerState < TP > ) -> R ,
186196 {
187197 let mut outer_state_lock = self . per_peer_state . write ( ) . unwrap ( ) ;
188198 let inner_state_lock = outer_state_lock. entry ( counterparty_node_id) . or_insert ( Mutex :: new (
189- PeerState :: new ( self . config . response_max_age_secs , Arc :: clone ( & self . time_provider ) ) ,
199+ PeerState :: new ( self . config . response_max_age_secs , self . time_provider . clone ( ) ) ,
190200 ) ) ;
191201 let mut peer_state_lock = inner_state_lock. lock ( ) . unwrap ( ) ;
192202
@@ -347,7 +357,7 @@ where
347357 action : ErrorAction :: IgnoreAndLog ( Level :: Error ) ,
348358 } ) ;
349359 let event_queue_notifier = self . pending_events . notifier ( ) ;
350- let handle_response = |peer_state : & mut PeerState | {
360+ let handle_response = |peer_state : & mut PeerState < TP > | {
351361 if let Some ( ( app_name, webhook_url, _) ) =
352362 peer_state. pending_set_webhook_requests . remove ( & request_id)
353363 {
@@ -449,13 +459,13 @@ where
449459 fn verify_notification_signature (
450460 & self , counterparty_node_id : PublicKey , signature_timestamp : & LSPSDateTime ,
451461 signature : & str , notification : & WebhookNotification ,
452- ) -> Result < bool , LSPS5ClientError > {
462+ ) -> Result < ( ) , LSPS5ClientError > {
453463 let now =
454464 LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
455465 let diff = signature_timestamp. abs_diff ( now) ;
456- let ten_minutes = 600 ;
457- if diff > ten_minutes {
458- return Err ( LSPS5ClientError :: InvalidTimestamp ( signature_timestamp . to_rfc3339 ( ) ) ) ;
466+ const MAX_TIMESTAMP_DRIFT_SECS : u64 = 600 ;
467+ if diff > MAX_TIMESTAMP_DRIFT_SECS {
468+ return Err ( LSPS5ClientError :: InvalidTimestamp ) ;
459469 }
460470
461471 let message = format ! (
@@ -465,7 +475,7 @@ where
465475 ) ;
466476
467477 if message_signing:: verify ( message. as_bytes ( ) , signature, & counterparty_node_id) {
468- Ok ( true )
478+ Ok ( ( ) )
469479 } else {
470480 Err ( LSPS5ClientError :: InvalidSignature )
471481 }
@@ -490,17 +500,10 @@ where
490500
491501 recent_signatures. push_back ( ( signature, now. clone ( ) ) ) ;
492502
493- let retention_duration = self . config . signature_config . retention_minutes * 60 ;
494- while let Some ( ( _, time) ) = recent_signatures. front ( ) {
495- if now. abs_diff ( time. clone ( ) ) > retention_duration. as_secs ( ) {
496- recent_signatures. pop_front ( ) ;
497- } else {
498- break ;
499- }
500- }
501-
502- while recent_signatures. len ( ) > self . config . signature_config . max_signatures {
503- recent_signatures. pop_front ( ) ;
503+ let retention_secs = self . config . signature_config . retention_minutes . as_secs ( ) ;
504+ recent_signatures. retain ( |( _, ts) | now. abs_diff ( ts. clone ( ) ) <= retention_secs) ;
505+ if recent_signatures. len ( ) > self . config . signature_config . max_signatures {
506+ recent_signatures. truncate ( self . config . signature_config . max_signatures ) ;
504507 }
505508 }
506509
@@ -513,15 +516,15 @@ where
513516 /// configured retention window.
514517 /// 4. Reconstructs the exact string
515518 /// `"LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {timestamp} I notify {body}"`
516- /// and verifies the zbase32 LN-style signature against the LSP’ s node ID.
519+ /// and verifies the zbase32 LN-style signature against the LSP' s node ID.
517520 ///
518521 /// # Parameters
519- /// - `counterparty_node_id`: the LSP’ s public key, used to verify the signature.
522+ /// - `counterparty_node_id`: the LSP' s public key, used to verify the signature.
520523 /// - `timestamp`: ISO8601 time when the LSP created the notification.
521524 /// - `signature`: the zbase32-encoded LN signature over timestamp+body.
522525 /// - `notification`: the [`WebhookNotification`] received from the LSP.
523526 ///
524- /// On success, emits [`LSPS5ClientEvent::WebhookNotificationReceived `].
527+ /// On success, returns the received [`WebhookNotification `].
525528 ///
526529 /// Failure reasons include:
527530 /// - Timestamp too old (drift > 10 minutes)
@@ -532,42 +535,31 @@ where
532535 /// event, before taking action on the notification. This guarantees that only authentic,
533536 /// non-replayed notifications reach your application.
534537 ///
535- /// [`LSPS5ClientEvent::WebhookNotificationReceived`]: super::event::LSPS5ClientEvent::WebhookNotificationReceived
536538 /// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
537539 /// [`WebhookNotification`]: super::msgs::WebhookNotification
538540 pub fn parse_webhook_notification (
539541 & self , counterparty_node_id : PublicKey , timestamp : & LSPSDateTime , signature : & str ,
540542 notification : & WebhookNotification ,
541- ) -> Result < ( ) , LSPS5ClientError > {
542- match self . verify_notification_signature (
543+ ) -> Result < WebhookNotification , LSPS5ClientError > {
544+ self . verify_notification_signature (
543545 counterparty_node_id,
544546 timestamp,
545547 signature,
546548 & notification,
547- ) {
548- Ok ( signature_valid) => {
549- let event_queue_notifier = self . pending_events . notifier ( ) ;
549+ ) ?;
550550
551- self . check_signature_exists ( signature) ?;
551+ self . check_signature_exists ( signature) ?;
552552
553- self . store_signature ( signature. to_string ( ) ) ;
553+ self . store_signature ( signature. to_string ( ) ) ;
554554
555- event_queue_notifier. enqueue ( LSPS5ClientEvent :: WebhookNotificationReceived {
556- counterparty_node_id,
557- notification : notification. clone ( ) ,
558- timestamp : timestamp. clone ( ) ,
559- signature_valid,
560- } ) ;
561- Ok ( ( ) )
562- } ,
563- Err ( e) => Err ( e) ,
564- }
555+ Ok ( notification. clone ( ) )
565556 }
566557}
567558
568- impl < ES : Deref > LSPSProtocolMessageHandler for LSPS5ClientHandler < ES >
559+ impl < ES : Deref , TP : Deref + Clone > LSPSProtocolMessageHandler for LSPS5ClientHandler < ES , TP >
569560where
570561 ES :: Target : EntropySource ,
562+ TP :: Target : TimeProvider ,
571563{
572564 type ProtocolMessage = LSPS5Message ;
573565 const PROTOCOL_NUMBER : Option < u16 > = Some ( 5 ) ;
@@ -592,8 +584,10 @@ mod tests {
592584 } ;
593585 use bitcoin:: { key:: Secp256k1 , secp256k1:: SecretKey } ;
594586
595- fn setup_test_client ( ) -> (
596- LSPS5ClientHandler < Arc < TestEntropy > > ,
587+ fn setup_test_client (
588+ time_provider : Arc < dyn TimeProvider > ,
589+ ) -> (
590+ LSPS5ClientHandler < Arc < TestEntropy > , Arc < dyn TimeProvider > > ,
597591 Arc < MessageQueue > ,
598592 Arc < EventQueue > ,
599593 PublicKey ,
@@ -602,7 +596,6 @@ mod tests {
602596 let test_entropy_source = Arc :: new ( TestEntropy { } ) ;
603597 let message_queue = Arc :: new ( MessageQueue :: new ( ) ) ;
604598 let event_queue = Arc :: new ( EventQueue :: new ( ) ) ;
605- let time_provider = Arc :: new ( DefaultTimeProvider ) ;
606599 let client = LSPS5ClientHandler :: new (
607600 test_entropy_source,
608601 message_queue. clone ( ) ,
@@ -622,7 +615,7 @@ mod tests {
622615
623616 #[ test]
624617 fn test_per_peer_state_isolation ( ) {
625- let ( client, _, _, peer_1, peer_2) = setup_test_client ( ) ;
618+ let ( client, _, _, peer_1, peer_2) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
626619
627620 let req_id_1 = client
628621 . set_webhook ( peer_1, "test-app-1" . to_string ( ) , "https://example.com/hook1" . to_string ( ) )
@@ -644,7 +637,7 @@ mod tests {
644637
645638 #[ test]
646639 fn test_pending_request_tracking ( ) {
647- let ( client, _, _, peer, _) = setup_test_client ( ) ;
640+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
648641 const APP_NAME : & str = "test-app" ;
649642 const WEBHOOK_URL : & str = "https://example.com/hook" ;
650643 let lsps5_app_name = LSPS5AppName :: from_string ( APP_NAME . to_string ( ) ) . unwrap ( ) ;
@@ -677,7 +670,7 @@ mod tests {
677670
678671 #[ test]
679672 fn test_handle_response_clears_pending_state ( ) {
680- let ( client, _, _, peer, _) = setup_test_client ( ) ;
673+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
681674
682675 let req_id = client
683676 . set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
@@ -707,7 +700,7 @@ mod tests {
707700
708701 #[ test]
709702 fn test_cleanup_expired_responses ( ) {
710- let ( client, _, _, _, _) = setup_test_client ( ) ;
703+ let ( client, _, _, _, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
711704 let time_provider = & client. time_provider ;
712705 const OLD_APP_NAME : & str = "test-app-old" ;
713706 const NEW_APP_NAME : & str = "test-app-new" ;
@@ -764,7 +757,7 @@ mod tests {
764757
765758 #[ test]
766759 fn test_unknown_request_id_handling ( ) {
767- let ( client, _message_queue, _, peer, _) = setup_test_client ( ) ;
760+ let ( client, _message_queue, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
768761
769762 let _valid_req = client
770763 . set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
0 commit comments