Skip to content

Commit dda00e6

Browse files
fixup: stop enqueuing ListWebhooksError. cleanup docs. expose new 'time' gated constructor
1 parent 6d0ac61 commit dda00e6

File tree

1 file changed

+44
-45
lines changed

1 file changed

+44
-45
lines changed

lightning-liquidity/src/lsps5/client.rs

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::lsps5::msgs::{
1717
LSPS5Message, LSPS5Request, LSPS5Response, ListWebhooksRequest, RemoveWebhookRequest,
1818
SetWebhookRequest, WebhookNotification,
1919
};
20+
#[cfg(feature = "time")]
21+
use crate::lsps5::service::DefaultTimeProvider;
2022
use crate::message_queue::MessageQueue;
2123
use crate::prelude::{new_hash_map, HashMap};
2224
use crate::sync::{Arc, Mutex, RwLock};
@@ -174,7 +176,7 @@ where
174176
TP::Target: TimeProvider,
175177
{
176178
/// Constructs an `LSPS5ClientHandler`.
177-
pub(crate) fn new(
179+
pub(crate) fn new_with_time_provider(
178180
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
179181
config: LSPS5ClientConfig, time_provider: TP,
180182
) -> Self {
@@ -273,13 +275,11 @@ where
273275
/// A unique `LSPSRequestId` for correlating the asynchronous response.
274276
///
275277
/// Response from the LSP peer will be provided asynchronously through a
276-
/// [`LSPS5Response::ListWebhooks`] or [`LSPS5Response::ListWebhooksError`] message, and this client
277-
/// will then enqueue either a [`WebhooksListed`] or [`WebhooksListFailed`] event.
278+
/// [`LSPS5Response::ListWebhooks`] message, and this client
279+
/// will then enqueue a [`WebhooksListed`] event.
278280
///
279281
/// [`WebhooksListed`]: super::event::LSPS5ClientEvent::WebhooksListed
280-
/// [`WebhooksListFailed`]: super::event::LSPS5ClientEvent::WebhooksListFailed
281282
/// [`LSPS5Response::ListWebhooks`]: super::msgs::LSPS5Response::ListWebhooks
282-
/// [`LSPS5Response::ListWebhooksError`]: super::msgs::LSPS5Response::ListWebhooksError
283283
pub fn list_webhooks(&self, counterparty_node_id: PublicKey) -> LSPSRequestId {
284284
let request_id = generate_request_id(&self.entropy_source);
285285
let now =
@@ -402,14 +402,6 @@ where
402402
});
403403
result = Ok(());
404404
},
405-
LSPS5Response::ListWebhooksError(e) => {
406-
event_queue_notifier.enqueue(LSPS5ClientEvent::WebhooksListFailed {
407-
counterparty_node_id: *counterparty_node_id,
408-
error: e.clone().into(),
409-
request_id,
410-
});
411-
result = Ok(());
412-
},
413405
_ => {
414406
result = Err(LightningError {
415407
err: "Unexpected response type for ListWebhooks".to_string(),
@@ -509,33 +501,21 @@ where
509501

510502
/// Parse and validate a webhook notification received from an LSP.
511503
///
512-
/// Implements the bLIP-55 / LSPS5 webhook delivery rules:
513-
/// 1. Parses `notification_json` into a `WebhookNotification` (JSON-RPC 2.0).
514-
/// 2. Checks that `timestamp` (from `x-lsps5-timestamp`) is within ±10 minutes of local time.
515-
/// 3. Ensures `signature` (from `x-lsps5-signature`) has not been replayed within the
516-
/// configured retention window.
517-
/// 4. Reconstructs the exact string
518-
/// `"LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {timestamp} I notify {body}"`
519-
/// and verifies the zbase32 LN-style signature against the LSP's node ID.
504+
/// Verifies the webhook delivery by parsing the notification JSON-RPC 2.0 format,
505+
/// checking the timestamp is within ±10 minutes, ensuring no signature replay within the retention window,
506+
/// and verifying the zbase32 LN-style signature against the LSP's node ID.
520507
///
521508
/// # Parameters
522509
/// - `counterparty_node_id`: the LSP's public key, used to verify the signature.
523510
/// - `timestamp`: ISO8601 time when the LSP created the notification.
524511
/// - `signature`: the zbase32-encoded LN signature over timestamp+body.
525512
/// - `notification`: the [`WebhookNotification`] received from the LSP.
526513
///
527-
/// On success, returns the received [`WebhookNotification`].
528-
///
529-
/// Failure reasons include:
530-
/// - Timestamp too old (drift > 10 minutes)
531-
/// - Replay attack detected (signature reused)
532-
/// - Invalid signature (crypto check fails)
514+
/// Returns the validated [`WebhookNotification`] or an error for invalid timestamp,
515+
/// replay attack, or signature verification failure.
533516
///
534-
/// Clients should call this method upon receiving a [`LSPS5ServiceEvent::SendWebhookNotification`]
535-
/// event, before taking action on the notification. This guarantees that only authentic,
536-
/// non-replayed notifications reach your application.
517+
/// Call this method before processing any webhook notification to ensure authenticity.
537518
///
538-
/// [`LSPS5ServiceEvent::SendWebhookNotification`]: super::event::LSPS5ServiceEvent::SendWebhookNotification
539519
/// [`WebhookNotification`]: super::msgs::WebhookNotification
540520
pub fn parse_webhook_notification(
541521
&self, counterparty_node_id: PublicKey, timestamp: &LSPSDateTime, signature: &str,
@@ -556,6 +536,27 @@ where
556536
}
557537
}
558538

539+
#[cfg(feature = "time")]
540+
impl<ES: Deref> LSPS5ClientHandler<ES, Arc<DefaultTimeProvider>>
541+
where
542+
ES::Target: EntropySource,
543+
{
544+
/// Constructs a `LSPS5ClientHandler` using [`DefaultTimeProvider`].
545+
#[allow(dead_code)]
546+
pub(crate) fn new(
547+
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
548+
config: LSPS5ClientConfig,
549+
) -> Self {
550+
Self::new_with_time_provider(
551+
entropy_source,
552+
pending_messages,
553+
pending_events,
554+
config,
555+
Arc::new(DefaultTimeProvider),
556+
)
557+
}
558+
}
559+
559560
impl<ES: Deref, TP: Deref + Clone> LSPSProtocolMessageHandler for LSPS5ClientHandler<ES, TP>
560561
where
561562
ES::Target: EntropySource,
@@ -577,16 +578,12 @@ mod tests {
577578

578579
use super::*;
579580
use crate::{
580-
lsps0::ser::LSPSRequestId,
581-
lsps5::{msgs::SetWebhookResponse, service::DefaultTimeProvider},
582-
tests::utils::TestEntropy,
581+
lsps0::ser::LSPSRequestId, lsps5::msgs::SetWebhookResponse, tests::utils::TestEntropy,
583582
};
584583
use bitcoin::{key::Secp256k1, secp256k1::SecretKey};
585584

586-
fn setup_test_client(
587-
time_provider: Arc<dyn TimeProvider>,
588-
) -> (
589-
LSPS5ClientHandler<Arc<TestEntropy>, Arc<dyn TimeProvider>>,
585+
fn setup_test_client() -> (
586+
LSPS5ClientHandler<Arc<TestEntropy>, Arc<DefaultTimeProvider>>,
590587
Arc<MessageQueue>,
591588
Arc<EventQueue>,
592589
PublicKey,
@@ -600,7 +597,6 @@ mod tests {
600597
Arc::clone(&message_queue),
601598
Arc::clone(&event_queue),
602599
LSPS5ClientConfig::default(),
603-
time_provider,
604600
);
605601

606602
let secp = Secp256k1::new();
@@ -614,7 +610,7 @@ mod tests {
614610

615611
#[test]
616612
fn test_per_peer_state_isolation() {
617-
let (client, _, _, peer_1, peer_2) = setup_test_client(Arc::new(DefaultTimeProvider));
613+
let (client, _, _, peer_1, peer_2) = setup_test_client();
618614

619615
let req_id_1 = client
620616
.set_webhook(peer_1, "test-app-1".to_string(), "https://example.com/hook1".to_string())
@@ -636,7 +632,7 @@ mod tests {
636632

637633
#[test]
638634
fn test_pending_request_tracking() {
639-
let (client, _, _, peer, _) = setup_test_client(Arc::new(DefaultTimeProvider));
635+
let (client, _, _, peer, _) = setup_test_client();
640636
const APP_NAME: &str = "test-app";
641637
const WEBHOOK_URL: &str = "https://example.com/hook";
642638
let lsps5_app_name = LSPS5AppName::from_string(APP_NAME.to_string()).unwrap();
@@ -669,7 +665,7 @@ mod tests {
669665

670666
#[test]
671667
fn test_handle_response_clears_pending_state() {
672-
let (client, _, _, peer, _) = setup_test_client(Arc::new(DefaultTimeProvider));
668+
let (client, _, _, peer, _) = setup_test_client();
673669

674670
let req_id = client
675671
.set_webhook(peer, "test-app".to_string(), "https://example.com/hook".to_string())
@@ -699,7 +695,7 @@ mod tests {
699695

700696
#[test]
701697
fn test_cleanup_expired_responses() {
702-
let (client, _, _, _, _) = setup_test_client(Arc::new(DefaultTimeProvider));
698+
let (client, _, _, _, _) = setup_test_client();
703699
let time_provider = &client.time_provider;
704700
const OLD_APP_NAME: &str = "test-app-old";
705701
const NEW_APP_NAME: &str = "test-app-new";
@@ -708,7 +704,10 @@ mod tests {
708704
let lsps5_new_app_name = LSPS5AppName::from_string(NEW_APP_NAME.to_string()).unwrap();
709705
let lsps5_webhook_url = LSPS5WebhookUrl::from_string(WEBHOOK_URL.to_string()).unwrap();
710706
let now = time_provider.duration_since_epoch();
711-
let mut peer_state = PeerState::new(Duration::from_secs(1800), Arc::clone(time_provider));
707+
let mut peer_state = PeerState::<Arc<DefaultTimeProvider>>::new(
708+
Duration::from_secs(1800),
709+
Arc::clone(time_provider),
710+
);
712711
peer_state.last_cleanup = Some(LSPSDateTime::new_from_duration_since_epoch(
713712
now.checked_sub(Duration::from_secs(120)).unwrap(),
714713
));
@@ -756,7 +755,7 @@ mod tests {
756755

757756
#[test]
758757
fn test_unknown_request_id_handling() {
759-
let (client, _message_queue, _, peer, _) = setup_test_client(Arc::new(DefaultTimeProvider));
758+
let (client, _message_queue, _, peer, _) = setup_test_client();
760759

761760
let _valid_req = client
762761
.set_webhook(peer, "test-app".to_string(), "https://example.com/hook".to_string())

0 commit comments

Comments
 (0)