@@ -83,18 +83,24 @@ impl Default for LSPS5ClientConfig {
83
83
}
84
84
}
85
85
86
- struct PeerState {
86
+ struct PeerState < TP : Deref >
87
+ where
88
+ TP :: Target : TimeProvider ,
89
+ {
87
90
pending_set_webhook_requests :
88
91
HashMap < LSPSRequestId , ( LSPS5AppName , LSPS5WebhookUrl , LSPSDateTime ) > ,
89
92
pending_list_webhooks_requests : HashMap < LSPSRequestId , LSPSDateTime > ,
90
93
pending_remove_webhook_requests : HashMap < LSPSRequestId , ( LSPS5AppName , LSPSDateTime ) > ,
91
94
last_cleanup : Option < LSPSDateTime > ,
92
95
max_age_secs : Duration ,
93
- time_provider : Arc < dyn TimeProvider > ,
96
+ time_provider : TP ,
94
97
}
95
98
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 {
98
104
Self {
99
105
pending_set_webhook_requests : new_hash_map ( ) ,
100
106
pending_list_webhooks_requests : new_hash_map ( ) ,
@@ -109,27 +115,29 @@ impl PeerState {
109
115
let now =
110
116
LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
111
117
// Only run cleanup once per minute to avoid excessive processing
112
- let minute = 60 ;
118
+ const CLEANUP_INTERVAL : Duration = Duration :: from_secs ( 60 ) ;
113
119
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 {
115
122
return ;
116
123
}
117
124
}
118
125
119
126
self . last_cleanup = Some ( now. clone ( ) ) ;
120
127
121
128
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
123
133
} ) ;
124
- self . pending_list_webhooks_requests
125
- . retain ( |_, timestamp| timestamp. abs_diff ( now. clone ( ) ) < self . max_age_secs . as_secs ( ) ) ;
126
134
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
128
136
} ) ;
129
137
}
130
138
}
131
139
132
- /// Client‐ side handler for the LSPS5 (bLIP-55) webhook registration protocol.
140
+ /// Client- side handler for the LSPS5 (bLIP-55) webhook registration protocol.
133
141
///
134
142
/// `LSPS5ClientHandler` is the primary interface for LSP clients
135
143
/// to register, list, and remove webhook endpoints with an LSP, and to parse
@@ -146,27 +154,29 @@ impl PeerState {
146
154
/// [`lsps5.set_webhook`]: super::msgs::LSPS5Request::SetWebhook
147
155
/// [`lsps5.list_webhooks`]: super::msgs::LSPS5Request::ListWebhooks
148
156
/// [`lsps5.remove_webhook`]: super::msgs::LSPS5Request::RemoveWebhook
149
- pub struct LSPS5ClientHandler < ES : Deref >
157
+ pub struct LSPS5ClientHandler < ES : Deref , TP : Deref + Clone >
150
158
where
151
159
ES :: Target : EntropySource ,
160
+ TP :: Target : TimeProvider ,
152
161
{
153
162
pending_messages : Arc < MessageQueue > ,
154
163
pending_events : Arc < EventQueue > ,
155
164
entropy_source : ES ,
156
- per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState > > > ,
165
+ per_peer_state : RwLock < HashMap < PublicKey , Mutex < PeerState < TP > > > > ,
157
166
config : LSPS5ClientConfig ,
158
- time_provider : Arc < dyn TimeProvider > ,
167
+ time_provider : TP ,
159
168
recent_signatures : Mutex < VecDeque < ( String , LSPSDateTime ) > > ,
160
169
}
161
170
162
- impl < ES : Deref > LSPS5ClientHandler < ES >
171
+ impl < ES : Deref , TP : Deref + Clone > LSPS5ClientHandler < ES , TP >
163
172
where
164
173
ES :: Target : EntropySource ,
174
+ TP :: Target : TimeProvider ,
165
175
{
166
176
/// Constructs an `LSPS5ClientHandler`.
167
177
pub ( crate ) fn new (
168
178
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 ,
170
180
) -> Self {
171
181
let max_signatures = config. signature_config . max_signatures . clone ( ) ;
172
182
Self {
@@ -182,11 +192,11 @@ where
182
192
183
193
fn with_peer_state < F , R > ( & self , counterparty_node_id : PublicKey , f : F ) -> R
184
194
where
185
- F : FnOnce ( & mut PeerState ) -> R ,
195
+ F : FnOnce ( & mut PeerState < TP > ) -> R ,
186
196
{
187
197
let mut outer_state_lock = self . per_peer_state . write ( ) . unwrap ( ) ;
188
198
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 ( ) ) ,
190
200
) ) ;
191
201
let mut peer_state_lock = inner_state_lock. lock ( ) . unwrap ( ) ;
192
202
@@ -347,7 +357,7 @@ where
347
357
action : ErrorAction :: IgnoreAndLog ( Level :: Error ) ,
348
358
} ) ;
349
359
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 > | {
351
361
if let Some ( ( app_name, webhook_url, _) ) =
352
362
peer_state. pending_set_webhook_requests . remove ( & request_id)
353
363
{
@@ -449,13 +459,13 @@ where
449
459
fn verify_notification_signature (
450
460
& self , counterparty_node_id : PublicKey , signature_timestamp : & LSPSDateTime ,
451
461
signature : & str , notification : & WebhookNotification ,
452
- ) -> Result < bool , LSPS5ClientError > {
462
+ ) -> Result < ( ) , LSPS5ClientError > {
453
463
let now =
454
464
LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
455
465
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 ) ;
459
469
}
460
470
461
471
let message = format ! (
@@ -465,7 +475,7 @@ where
465
475
) ;
466
476
467
477
if message_signing:: verify ( message. as_bytes ( ) , signature, & counterparty_node_id) {
468
- Ok ( true )
478
+ Ok ( ( ) )
469
479
} else {
470
480
Err ( LSPS5ClientError :: InvalidSignature )
471
481
}
@@ -490,17 +500,10 @@ where
490
500
491
501
recent_signatures. push_back ( ( signature, now. clone ( ) ) ) ;
492
502
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 ) ;
504
507
}
505
508
}
506
509
@@ -513,15 +516,15 @@ where
513
516
/// configured retention window.
514
517
/// 4. Reconstructs the exact string
515
518
/// `"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.
517
520
///
518
521
/// # 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.
520
523
/// - `timestamp`: ISO8601 time when the LSP created the notification.
521
524
/// - `signature`: the zbase32-encoded LN signature over timestamp+body.
522
525
/// - `notification`: the [`WebhookNotification`] received from the LSP.
523
526
///
524
- /// On success, emits [`LSPS5ClientEvent::WebhookNotificationReceived `].
527
+ /// On success, returns the received [`WebhookNotification `].
525
528
///
526
529
/// Failure reasons include:
527
530
/// - Timestamp too old (drift > 10 minutes)
@@ -532,42 +535,31 @@ where
532
535
/// event, before taking action on the notification. This guarantees that only authentic,
533
536
/// non-replayed notifications reach your application.
534
537
///
535
- /// [`LSPS5ClientEvent::WebhookNotificationReceived`]: super::event::LSPS5ClientEvent::WebhookNotificationReceived
536
538
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
537
539
/// [`WebhookNotification`]: super::msgs::WebhookNotification
538
540
pub fn parse_webhook_notification (
539
541
& self , counterparty_node_id : PublicKey , timestamp : & LSPSDateTime , signature : & str ,
540
542
notification : & WebhookNotification ,
541
- ) -> Result < ( ) , LSPS5ClientError > {
542
- match self . verify_notification_signature (
543
+ ) -> Result < WebhookNotification , LSPS5ClientError > {
544
+ self . verify_notification_signature (
543
545
counterparty_node_id,
544
546
timestamp,
545
547
signature,
546
548
& notification,
547
- ) {
548
- Ok ( signature_valid) => {
549
- let event_queue_notifier = self . pending_events . notifier ( ) ;
549
+ ) ?;
550
550
551
- self . check_signature_exists ( signature) ?;
551
+ self . check_signature_exists ( signature) ?;
552
552
553
- self . store_signature ( signature. to_string ( ) ) ;
553
+ self . store_signature ( signature. to_string ( ) ) ;
554
554
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 ( ) )
565
556
}
566
557
}
567
558
568
- impl < ES : Deref > LSPSProtocolMessageHandler for LSPS5ClientHandler < ES >
559
+ impl < ES : Deref , TP : Deref + Clone > LSPSProtocolMessageHandler for LSPS5ClientHandler < ES , TP >
569
560
where
570
561
ES :: Target : EntropySource ,
562
+ TP :: Target : TimeProvider ,
571
563
{
572
564
type ProtocolMessage = LSPS5Message ;
573
565
const PROTOCOL_NUMBER : Option < u16 > = Some ( 5 ) ;
@@ -592,8 +584,10 @@ mod tests {
592
584
} ;
593
585
use bitcoin:: { key:: Secp256k1 , secp256k1:: SecretKey } ;
594
586
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 > > ,
597
591
Arc < MessageQueue > ,
598
592
Arc < EventQueue > ,
599
593
PublicKey ,
@@ -602,7 +596,6 @@ mod tests {
602
596
let test_entropy_source = Arc :: new ( TestEntropy { } ) ;
603
597
let message_queue = Arc :: new ( MessageQueue :: new ( ) ) ;
604
598
let event_queue = Arc :: new ( EventQueue :: new ( ) ) ;
605
- let time_provider = Arc :: new ( DefaultTimeProvider ) ;
606
599
let client = LSPS5ClientHandler :: new (
607
600
test_entropy_source,
608
601
message_queue. clone ( ) ,
@@ -622,7 +615,7 @@ mod tests {
622
615
623
616
#[ test]
624
617
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 ) ) ;
626
619
627
620
let req_id_1 = client
628
621
. set_webhook ( peer_1, "test-app-1" . to_string ( ) , "https://example.com/hook1" . to_string ( ) )
@@ -644,7 +637,7 @@ mod tests {
644
637
645
638
#[ test]
646
639
fn test_pending_request_tracking ( ) {
647
- let ( client, _, _, peer, _) = setup_test_client ( ) ;
640
+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
648
641
const APP_NAME : & str = "test-app" ;
649
642
const WEBHOOK_URL : & str = "https://example.com/hook" ;
650
643
let lsps5_app_name = LSPS5AppName :: from_string ( APP_NAME . to_string ( ) ) . unwrap ( ) ;
@@ -677,7 +670,7 @@ mod tests {
677
670
678
671
#[ test]
679
672
fn test_handle_response_clears_pending_state ( ) {
680
- let ( client, _, _, peer, _) = setup_test_client ( ) ;
673
+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
681
674
682
675
let req_id = client
683
676
. set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
@@ -707,7 +700,7 @@ mod tests {
707
700
708
701
#[ test]
709
702
fn test_cleanup_expired_responses ( ) {
710
- let ( client, _, _, _, _) = setup_test_client ( ) ;
703
+ let ( client, _, _, _, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
711
704
let time_provider = & client. time_provider ;
712
705
const OLD_APP_NAME : & str = "test-app-old" ;
713
706
const NEW_APP_NAME : & str = "test-app-new" ;
@@ -764,7 +757,7 @@ mod tests {
764
757
765
758
#[ test]
766
759
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 ) ) ;
768
761
769
762
let _valid_req = client
770
763
. set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
0 commit comments