@@ -83,18 +83,18 @@ impl Default for LSPS5ClientConfig {
83
83
}
84
84
}
85
85
86
- struct PeerState {
86
+ struct PeerState < TP : TimeProvider + Send + Sync > {
87
87
pending_set_webhook_requests :
88
88
HashMap < LSPSRequestId , ( LSPS5AppName , LSPS5WebhookUrl , LSPSDateTime ) > ,
89
89
pending_list_webhooks_requests : HashMap < LSPSRequestId , LSPSDateTime > ,
90
90
pending_remove_webhook_requests : HashMap < LSPSRequestId , ( LSPS5AppName , LSPSDateTime ) > ,
91
91
last_cleanup : Option < LSPSDateTime > ,
92
92
max_age_secs : Duration ,
93
- time_provider : Arc < dyn TimeProvider > ,
93
+ time_provider : Arc < TP > ,
94
94
}
95
95
96
- impl PeerState {
97
- fn new ( max_age_secs : Duration , time_provider : Arc < dyn TimeProvider > ) -> Self {
96
+ impl < TP : TimeProvider + Send + Sync > PeerState < TP > {
97
+ fn new ( max_age_secs : Duration , time_provider : Arc < TP > ) -> Self {
98
98
Self {
99
99
pending_set_webhook_requests : new_hash_map ( ) ,
100
100
pending_list_webhooks_requests : new_hash_map ( ) ,
@@ -109,27 +109,29 @@ impl PeerState {
109
109
let now =
110
110
LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
111
111
// Only run cleanup once per minute to avoid excessive processing
112
- let minute = 60 ;
112
+ const CLEANUP_INTERVAL : Duration = Duration :: from_secs ( 60 ) ;
113
113
if let Some ( last_cleanup) = & self . last_cleanup {
114
- if now. abs_diff ( last_cleanup. clone ( ) ) < minute {
114
+ let time_since_last_cleanup = Duration :: from_secs ( now. abs_diff ( last_cleanup. clone ( ) ) ) ;
115
+ if time_since_last_cleanup < CLEANUP_INTERVAL {
115
116
return ;
116
117
}
117
118
}
118
119
119
120
self . last_cleanup = Some ( now. clone ( ) ) ;
120
121
121
122
self . pending_set_webhook_requests . retain ( |_, ( _, _, timestamp) | {
122
- timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( )
123
+ Duration :: from_secs ( timestamp. abs_diff ( now. clone ( ) ) ) < self . max_age_secs
124
+ } ) ;
125
+ self . pending_list_webhooks_requests . retain ( |_, timestamp| {
126
+ Duration :: from_secs ( timestamp. abs_diff ( now. clone ( ) ) ) < self . max_age_secs
123
127
} ) ;
124
- self . pending_list_webhooks_requests
125
- . retain ( |_, timestamp| timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( ) ) ;
126
128
self . pending_remove_webhook_requests . retain ( |_, ( _, timestamp) | {
127
- timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( )
129
+ Duration :: from_secs ( timestamp. abs_diff ( now. clone ( ) ) ) < self . max_age_secs
128
130
} ) ;
129
131
}
130
132
}
131
133
132
- /// Client‐ side handler for the LSPS5 (bLIP-55) webhook registration protocol.
134
+ /// Client- side handler for the LSPS5 (bLIP-55) webhook registration protocol.
133
135
///
134
136
/// `LSPS5ClientHandler` is the primary interface for LSP clients
135
137
/// to register, list, and remove webhook endpoints with an LSP, and to parse
@@ -146,27 +148,27 @@ impl PeerState {
146
148
/// [`lsps5.set_webhook`]: super::msgs::LSPS5Request::SetWebhook
147
149
/// [`lsps5.list_webhooks`]: super::msgs::LSPS5Request::ListWebhooks
148
150
/// [`lsps5.remove_webhook`]: super::msgs::LSPS5Request::RemoveWebhook
149
- pub struct LSPS5ClientHandler < ES : Deref >
151
+ pub struct LSPS5ClientHandler < ES : Deref , TP : TimeProvider + Send + Sync >
150
152
where
151
153
ES :: Target : EntropySource ,
152
154
{
153
155
pending_messages : Arc < MessageQueue > ,
154
156
pending_events : Arc < EventQueue > ,
155
157
entropy_source : ES ,
156
- per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState > > > ,
158
+ per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState < TP > > > > ,
157
159
config : LSPS5ClientConfig ,
158
- time_provider : Arc < dyn TimeProvider > ,
160
+ time_provider : Arc < TP > ,
159
161
recent_signatures : Mutex < VecDeque < ( String , LSPSDateTime ) > > ,
160
162
}
161
163
162
- impl < ES : Deref > LSPS5ClientHandler < ES >
164
+ impl < ES : Deref , TP : TimeProvider + Send + Sync > LSPS5ClientHandler < ES , TP >
163
165
where
164
166
ES :: Target : EntropySource ,
165
167
{
166
168
/// Constructs an `LSPS5ClientHandler`.
167
169
pub ( crate ) fn new (
168
170
entropy_source : ES , pending_messages : Arc < MessageQueue > , pending_events : Arc < EventQueue > ,
169
- config : LSPS5ClientConfig , time_provider : Arc < dyn TimeProvider > ,
171
+ config : LSPS5ClientConfig , time_provider : Arc < TP > ,
170
172
) -> Self {
171
173
let max_signatures = config. signature_config . max_signatures . clone ( ) ;
172
174
Self {
@@ -182,7 +184,7 @@ where
182
184
183
185
fn with_peer_state < F , R > ( & self , counterparty_node_id : PublicKey , f : F ) -> R
184
186
where
185
- F : FnOnce ( & mut PeerState ) -> R ,
187
+ F : FnOnce ( & mut PeerState < TP > ) -> R ,
186
188
{
187
189
let mut outer_state_lock = self . per_peer_state . write ( ) . unwrap ( ) ;
188
190
let inner_state_lock = outer_state_lock. entry ( counterparty_node_id) . or_insert ( Mutex :: new (
@@ -347,7 +349,7 @@ where
347
349
action : ErrorAction :: IgnoreAndLog ( Level :: Error ) ,
348
350
} ) ;
349
351
let event_queue_notifier = self . pending_events . notifier ( ) ;
350
- let handle_response = |peer_state : & mut PeerState | {
352
+ let handle_response = |peer_state : & mut PeerState < TP > | {
351
353
if let Some ( ( app_name, webhook_url, _) ) =
352
354
peer_state. pending_set_webhook_requests . remove ( & request_id)
353
355
{
@@ -449,13 +451,13 @@ where
449
451
fn verify_notification_signature (
450
452
& self , counterparty_node_id : PublicKey , signature_timestamp : & LSPSDateTime ,
451
453
signature : & str , notification : & WebhookNotification ,
452
- ) -> Result < bool , LSPS5ClientError > {
454
+ ) -> Result < ( ) , LSPS5ClientError > {
453
455
let now =
454
456
LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
455
457
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 ( ) ) ) ;
458
+ const MAX_TIMESTAMP_DRIFT_SECS : u64 = 600 ;
459
+ if diff > MAX_TIMESTAMP_DRIFT_SECS {
460
+ return Err ( LSPS5ClientError :: InvalidTimestamp ) ;
459
461
}
460
462
461
463
let message = format ! (
@@ -465,7 +467,7 @@ where
465
467
) ;
466
468
467
469
if message_signing:: verify ( message. as_bytes ( ) , signature, & counterparty_node_id) {
468
- Ok ( true )
470
+ Ok ( ( ) )
469
471
} else {
470
472
Err ( LSPS5ClientError :: InvalidSignature )
471
473
}
@@ -490,17 +492,10 @@ where
490
492
491
493
recent_signatures. push_back ( ( signature, now. clone ( ) ) ) ;
492
494
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 ( ) ;
495
+ let retention_secs = self . config . signature_config . retention_minutes . as_secs ( ) ;
496
+ recent_signatures. retain ( |( _, ts) | now. abs_diff ( ts. clone ( ) ) <= retention_secs) ;
497
+ if recent_signatures. len ( ) > self . config . signature_config . max_signatures {
498
+ recent_signatures. truncate ( self . config . signature_config . max_signatures ) ;
504
499
}
505
500
}
506
501
@@ -513,15 +508,15 @@ where
513
508
/// configured retention window.
514
509
/// 4. Reconstructs the exact string
515
510
/// `"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.
511
+ /// and verifies the zbase32 LN-style signature against the LSP' s node ID.
517
512
///
518
513
/// # Parameters
519
- /// - `counterparty_node_id`: the LSP’ s public key, used to verify the signature.
514
+ /// - `counterparty_node_id`: the LSP' s public key, used to verify the signature.
520
515
/// - `timestamp`: ISO8601 time when the LSP created the notification.
521
516
/// - `signature`: the zbase32-encoded LN signature over timestamp+body.
522
517
/// - `notification`: the [`WebhookNotification`] received from the LSP.
523
518
///
524
- /// On success, emits [`LSPS5ClientEvent::WebhookNotificationReceived `].
519
+ /// On success, returns the received [`WebhookNotification `].
525
520
///
526
521
/// Failure reasons include:
527
522
/// - Timestamp too old (drift > 10 minutes)
@@ -532,40 +527,29 @@ where
532
527
/// event, before taking action on the notification. This guarantees that only authentic,
533
528
/// non-replayed notifications reach your application.
534
529
///
535
- /// [`LSPS5ClientEvent::WebhookNotificationReceived`]: super::event::LSPS5ClientEvent::WebhookNotificationReceived
536
530
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
537
531
/// [`WebhookNotification`]: super::msgs::WebhookNotification
538
532
pub fn parse_webhook_notification (
539
533
& self , counterparty_node_id : PublicKey , timestamp : & LSPSDateTime , signature : & str ,
540
534
notification : & WebhookNotification ,
541
- ) -> Result < ( ) , LSPS5ClientError > {
542
- match self . verify_notification_signature (
535
+ ) -> Result < WebhookNotification , LSPS5ClientError > {
536
+ self . verify_notification_signature (
543
537
counterparty_node_id,
544
538
timestamp,
545
539
signature,
546
540
& notification,
547
- ) {
548
- Ok ( signature_valid) => {
549
- let event_queue_notifier = self . pending_events . notifier ( ) ;
541
+ ) ?;
550
542
551
- self . check_signature_exists ( signature) ?;
543
+ self . check_signature_exists ( signature) ?;
552
544
553
- self . store_signature ( signature. to_string ( ) ) ;
545
+ self . store_signature ( signature. to_string ( ) ) ;
554
546
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
- }
547
+ Ok ( notification. clone ( ) )
565
548
}
566
549
}
567
550
568
- impl < ES : Deref > LSPSProtocolMessageHandler for LSPS5ClientHandler < ES >
551
+ impl < ES : Deref , TP : TimeProvider + Send + Sync > LSPSProtocolMessageHandler
552
+ for LSPS5ClientHandler < ES , TP >
569
553
where
570
554
ES :: Target : EntropySource ,
571
555
{
@@ -592,8 +576,10 @@ mod tests {
592
576
} ;
593
577
use bitcoin:: { key:: Secp256k1 , secp256k1:: SecretKey } ;
594
578
595
- fn setup_test_client ( ) -> (
596
- LSPS5ClientHandler < Arc < TestEntropy > > ,
579
+ fn setup_test_client < TP : TimeProvider + Send + Sync + ' static > (
580
+ time_provider : Arc < TP > ,
581
+ ) -> (
582
+ LSPS5ClientHandler < Arc < TestEntropy > , TP > ,
597
583
Arc < MessageQueue > ,
598
584
Arc < EventQueue > ,
599
585
PublicKey ,
@@ -602,7 +588,6 @@ mod tests {
602
588
let test_entropy_source = Arc :: new ( TestEntropy { } ) ;
603
589
let message_queue = Arc :: new ( MessageQueue :: new ( ) ) ;
604
590
let event_queue = Arc :: new ( EventQueue :: new ( ) ) ;
605
- let time_provider = Arc :: new ( DefaultTimeProvider ) ;
606
591
let client = LSPS5ClientHandler :: new (
607
592
test_entropy_source,
608
593
message_queue. clone ( ) ,
@@ -622,7 +607,7 @@ mod tests {
622
607
623
608
#[ test]
624
609
fn test_per_peer_state_isolation ( ) {
625
- let ( client, _, _, peer_1, peer_2) = setup_test_client ( ) ;
610
+ let ( client, _, _, peer_1, peer_2) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
626
611
627
612
let req_id_1 = client
628
613
. set_webhook ( peer_1, "test-app-1" . to_string ( ) , "https://example.com/hook1" . to_string ( ) )
@@ -644,7 +629,7 @@ mod tests {
644
629
645
630
#[ test]
646
631
fn test_pending_request_tracking ( ) {
647
- let ( client, _, _, peer, _) = setup_test_client ( ) ;
632
+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
648
633
const APP_NAME : & str = "test-app" ;
649
634
const WEBHOOK_URL : & str = "https://example.com/hook" ;
650
635
let lsps5_app_name = LSPS5AppName :: from_string ( APP_NAME . to_string ( ) ) . unwrap ( ) ;
@@ -677,7 +662,7 @@ mod tests {
677
662
678
663
#[ test]
679
664
fn test_handle_response_clears_pending_state ( ) {
680
- let ( client, _, _, peer, _) = setup_test_client ( ) ;
665
+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
681
666
682
667
let req_id = client
683
668
. set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
@@ -707,7 +692,7 @@ mod tests {
707
692
708
693
#[ test]
709
694
fn test_cleanup_expired_responses ( ) {
710
- let ( client, _, _, _, _) = setup_test_client ( ) ;
695
+ let ( client, _, _, _, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
711
696
let time_provider = & client. time_provider ;
712
697
const OLD_APP_NAME : & str = "test-app-old" ;
713
698
const NEW_APP_NAME : & str = "test-app-new" ;
@@ -764,7 +749,7 @@ mod tests {
764
749
765
750
#[ test]
766
751
fn test_unknown_request_id_handling ( ) {
767
- let ( client, _message_queue, _, peer, _) = setup_test_client ( ) ;
752
+ let ( client, _message_queue, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
768
753
769
754
let _valid_req = client
770
755
. set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
0 commit comments