Skip to content

Commit a39cced

Browse files
Add tests for LSPS5 client and service.
1 parent 9494b65 commit a39cced

File tree

6 files changed

+1562
-26
lines changed

6 files changed

+1562
-26
lines changed

lightning-background-processor/src/lib.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput};
647647
/// # use std::sync::atomic::{AtomicBool, Ordering};
648648
/// # use std::time::SystemTime;
649649
/// # use lightning_background_processor::{process_events_async, GossipSync};
650+
/// # use lightning_liquidity::lsps5::service::TimeProvider;
650651
/// # struct Logger {}
651652
/// # impl lightning::util::logger::Logger for Logger {
652653
/// # fn log(&self, _record: lightning::util::logger::Record) {}
@@ -658,6 +659,16 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput};
658659
/// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) }
659660
/// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result<Vec<String>> { Ok(Vec::new()) }
660661
/// # }
662+
/// #
663+
/// # use core::time::Duration;
664+
/// # struct DefaultTimeProvider;
665+
/// #
666+
/// # impl TimeProvider for DefaultTimeProvider {
667+
/// # fn duration_since_epoch(&self) -> Duration {
668+
/// # use std::time::{SystemTime, UNIX_EPOCH};
669+
/// # SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch")
670+
/// # }
671+
/// # }
661672
/// # struct EventHandler {}
662673
/// # impl EventHandler {
663674
/// # async fn handle_event(&self, _: lightning::events::Event) -> Result<(), ReplayEvent> { Ok(()) }
@@ -673,7 +684,7 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput};
673684
/// # type P2PGossipSync<UL> = lightning::routing::gossip::P2PGossipSync<Arc<NetworkGraph>, Arc<UL>, Arc<Logger>>;
674685
/// # type ChannelManager<B, F, FE> = lightning::ln::channelmanager::SimpleArcChannelManager<ChainMonitor<B, F, FE>, B, FE, Logger>;
675686
/// # type OnionMessenger<B, F, FE> = lightning::onion_message::messenger::OnionMessenger<Arc<lightning::sign::KeysManager>, Arc<lightning::sign::KeysManager>, Arc<Logger>, Arc<ChannelManager<B, F, FE>>, Arc<lightning::onion_message::messenger::DefaultMessageRouter<Arc<NetworkGraph>, Arc<Logger>, Arc<lightning::sign::KeysManager>>>, Arc<ChannelManager<B, F, FE>>, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler>;
676-
/// # type LiquidityManager<B, F, FE> = lightning_liquidity::LiquidityManager<Arc<lightning::sign::KeysManager>, Arc<ChannelManager<B, F, FE>>, Arc<F>>;
687+
/// # type LiquidityManager<B, F, FE> = lightning_liquidity::LiquidityManager<Arc<lightning::sign::KeysManager>, Arc<ChannelManager<B, F, FE>>, Arc<F>, Arc<DefaultTimeProvider>>;
677688
/// # type Scorer = RwLock<lightning::routing::scoring::ProbabilisticScorer<Arc<NetworkGraph>, Arc<Logger>>>;
678689
/// # type PeerManager<B, F, FE, UL> = lightning::ln::peer_handler::SimpleArcPeerManager<SocketDescriptor, ChainMonitor<B, F, FE>, B, FE, Arc<UL>, Logger>;
679690
/// #
@@ -1285,8 +1296,12 @@ mod tests {
12851296
IgnoringMessageHandler,
12861297
>;
12871298

1288-
type LM =
1289-
LiquidityManager<Arc<KeysManager>, Arc<ChannelManager>, Arc<dyn Filter + Sync + Send>>;
1299+
type LM = LiquidityManager<
1300+
Arc<KeysManager>,
1301+
Arc<ChannelManager>,
1302+
Arc<dyn Filter + Sync + Send>,
1303+
Arc<DefaultTimeProvider>,
1304+
>;
12901305

12911306
struct Node {
12921307
node: Arc<ChannelManager>,

lightning-liquidity/src/lsps5/client.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,210 @@ where
570570
self.handle_message(message, lsp_node_id)
571571
}
572572
}
573+
574+
#[cfg(test)]
575+
mod tests {
576+
#![cfg(all(test, feature = "time"))]
577+
use core::time::Duration;
578+
579+
use super::*;
580+
use crate::{
581+
lsps0::ser::LSPSRequestId,
582+
lsps5::{msgs::SetWebhookResponse, service::DefaultTimeProvider},
583+
tests::utils::TestEntropy,
584+
};
585+
use bitcoin::{key::Secp256k1, secp256k1::SecretKey};
586+
587+
fn setup_test_client(
588+
time_provider: Arc<dyn TimeProvider>,
589+
) -> (
590+
LSPS5ClientHandler<Arc<TestEntropy>, Arc<dyn TimeProvider>>,
591+
Arc<MessageQueue>,
592+
Arc<EventQueue>,
593+
PublicKey,
594+
PublicKey,
595+
) {
596+
let test_entropy_source = Arc::new(TestEntropy {});
597+
let message_queue = Arc::new(MessageQueue::new());
598+
let event_queue = Arc::new(EventQueue::new());
599+
let client = LSPS5ClientHandler::new(
600+
test_entropy_source,
601+
message_queue.clone(),
602+
event_queue.clone(),
603+
LSPS5ClientConfig::default(),
604+
time_provider,
605+
);
606+
607+
let secp = Secp256k1::new();
608+
let secret_key_1 = SecretKey::from_slice(&[42u8; 32]).unwrap();
609+
let secret_key_2 = SecretKey::from_slice(&[43u8; 32]).unwrap();
610+
let peer_1 = PublicKey::from_secret_key(&secp, &secret_key_1);
611+
let peer_2 = PublicKey::from_secret_key(&secp, &secret_key_2);
612+
613+
(client, message_queue, event_queue, peer_1, peer_2)
614+
}
615+
616+
#[test]
617+
fn test_per_peer_state_isolation() {
618+
let (client, _, _, peer_1, peer_2) = setup_test_client(Arc::new(DefaultTimeProvider));
619+
620+
let req_id_1 = client
621+
.set_webhook(peer_1, "test-app-1".to_string(), "https://example.com/hook1".to_string())
622+
.unwrap();
623+
let req_id_2 = client
624+
.set_webhook(peer_2, "test-app-2".to_string(), "https://example.com/hook2".to_string())
625+
.unwrap();
626+
627+
{
628+
let outer_state_lock = client.per_peer_state.read().unwrap();
629+
630+
let peer_1_state = outer_state_lock.get(&peer_1).unwrap().lock().unwrap();
631+
assert!(peer_1_state.pending_set_webhook_requests.contains_key(&req_id_1));
632+
633+
let peer_2_state = outer_state_lock.get(&peer_2).unwrap().lock().unwrap();
634+
assert!(peer_2_state.pending_set_webhook_requests.contains_key(&req_id_2));
635+
}
636+
}
637+
638+
#[test]
639+
fn test_pending_request_tracking() {
640+
let (client, _, _, peer, _) = setup_test_client(Arc::new(DefaultTimeProvider));
641+
const APP_NAME: &str = "test-app";
642+
const WEBHOOK_URL: &str = "https://example.com/hook";
643+
let lsps5_app_name = LSPS5AppName::from_string(APP_NAME.to_string()).unwrap();
644+
let lsps5_webhook_url = LSPS5WebhookUrl::from_string(WEBHOOK_URL.to_string()).unwrap();
645+
let set_req_id =
646+
client.set_webhook(peer, APP_NAME.to_string(), WEBHOOK_URL.to_string()).unwrap();
647+
let list_req_id = client.list_webhooks(peer);
648+
let remove_req_id = client.remove_webhook(peer, "test-app".to_string()).unwrap();
649+
650+
{
651+
let outer_state_lock = client.per_peer_state.read().unwrap();
652+
let peer_state = outer_state_lock.get(&peer).unwrap().lock().unwrap();
653+
assert_eq!(
654+
peer_state.pending_set_webhook_requests.get(&set_req_id).unwrap(),
655+
&(
656+
lsps5_app_name.clone(),
657+
lsps5_webhook_url,
658+
peer_state.pending_set_webhook_requests.get(&set_req_id).unwrap().2.clone()
659+
)
660+
);
661+
662+
assert!(peer_state.pending_list_webhooks_requests.contains_key(&list_req_id));
663+
664+
assert_eq!(
665+
peer_state.pending_remove_webhook_requests.get(&remove_req_id).unwrap().0,
666+
lsps5_app_name
667+
);
668+
}
669+
}
670+
671+
#[test]
672+
fn test_handle_response_clears_pending_state() {
673+
let (client, _, _, peer, _) = setup_test_client(Arc::new(DefaultTimeProvider));
674+
675+
let req_id = client
676+
.set_webhook(peer, "test-app".to_string(), "https://example.com/hook".to_string())
677+
.unwrap();
678+
679+
let response = LSPS5Response::SetWebhook(SetWebhookResponse {
680+
num_webhooks: 1,
681+
max_webhooks: 5,
682+
no_change: false,
683+
});
684+
let response_msg = LSPS5Message::Response(req_id.clone(), response);
685+
686+
{
687+
let outer_state_lock = client.per_peer_state.read().unwrap();
688+
let peer_state = outer_state_lock.get(&peer).unwrap().lock().unwrap();
689+
assert!(peer_state.pending_set_webhook_requests.contains_key(&req_id));
690+
}
691+
692+
client.handle_message(response_msg, &peer).unwrap();
693+
694+
{
695+
let outer_state_lock = client.per_peer_state.read().unwrap();
696+
let peer_state = outer_state_lock.get(&peer).unwrap().lock().unwrap();
697+
assert!(!peer_state.pending_set_webhook_requests.contains_key(&req_id));
698+
}
699+
}
700+
701+
#[test]
702+
fn test_cleanup_expired_responses() {
703+
let (client, _, _, _, _) = setup_test_client(Arc::new(DefaultTimeProvider));
704+
let time_provider = &client.time_provider;
705+
const OLD_APP_NAME: &str = "test-app-old";
706+
const NEW_APP_NAME: &str = "test-app-new";
707+
const WEBHOOK_URL: &str = "https://example.com/hook";
708+
let lsps5_old_app_name = LSPS5AppName::from_string(OLD_APP_NAME.to_string()).unwrap();
709+
let lsps5_new_app_name = LSPS5AppName::from_string(NEW_APP_NAME.to_string()).unwrap();
710+
let lsps5_webhook_url = LSPS5WebhookUrl::from_string(WEBHOOK_URL.to_string()).unwrap();
711+
let now = time_provider.duration_since_epoch();
712+
let mut peer_state = PeerState::new(Duration::from_secs(1800), time_provider.clone());
713+
peer_state.last_cleanup = Some(LSPSDateTime::new_from_duration_since_epoch(
714+
now.checked_sub(Duration::from_secs(120)).unwrap(),
715+
));
716+
717+
let old_request_id = LSPSRequestId("test:request:old".to_string());
718+
let new_request_id = LSPSRequestId("test:request:new".to_string());
719+
720+
// Add an old request (should be removed during cleanup)
721+
peer_state.pending_set_webhook_requests.insert(
722+
old_request_id.clone(),
723+
(
724+
lsps5_old_app_name,
725+
lsps5_webhook_url.clone(),
726+
LSPSDateTime::new_from_duration_since_epoch(
727+
now.checked_sub(Duration::from_secs(7200)).unwrap(),
728+
),
729+
), // 2 hours old
730+
);
731+
732+
// Add a recent request (should be kept)
733+
peer_state.pending_set_webhook_requests.insert(
734+
new_request_id.clone(),
735+
(
736+
lsps5_new_app_name,
737+
lsps5_webhook_url,
738+
LSPSDateTime::new_from_duration_since_epoch(
739+
now.checked_sub(Duration::from_secs(600)).unwrap(),
740+
),
741+
), // 10 minutes old
742+
);
743+
744+
peer_state.cleanup_expired_responses();
745+
746+
assert!(!peer_state.pending_set_webhook_requests.contains_key(&old_request_id));
747+
assert!(peer_state.pending_set_webhook_requests.contains_key(&new_request_id));
748+
749+
let cleanup_age = if let Some(last_cleanup) = peer_state.last_cleanup {
750+
LSPSDateTime::new_from_duration_since_epoch(time_provider.duration_since_epoch())
751+
.abs_diff(last_cleanup)
752+
} else {
753+
0
754+
};
755+
assert!(cleanup_age < 10);
756+
}
757+
758+
#[test]
759+
fn test_unknown_request_id_handling() {
760+
let (client, _message_queue, _, peer, _) = setup_test_client(Arc::new(DefaultTimeProvider));
761+
762+
let _valid_req = client
763+
.set_webhook(peer, "test-app".to_string(), "https://example.com/hook".to_string())
764+
.unwrap();
765+
766+
let unknown_req_id = LSPSRequestId("unknown:request:id".to_string());
767+
let response = LSPS5Response::SetWebhook(SetWebhookResponse {
768+
num_webhooks: 1,
769+
max_webhooks: 5,
770+
no_change: false,
771+
});
772+
let response_msg = LSPS5Message::Response(unknown_req_id, response);
773+
774+
let result = client.handle_message(response_msg, &peer);
775+
assert!(result.is_err());
776+
let error = result.unwrap_err();
777+
assert!(error.err.to_lowercase().contains("unknown request id"));
778+
}
779+
}

lightning-liquidity/tests/common/mod.rs

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
#![cfg(test)]
1+
#![cfg(all(test, feature = "time"))]
22
// TODO: remove these flags and unused code once we know what we'll need.
33
#![allow(dead_code)]
44
#![allow(unused_imports)]
55
#![allow(unused_macros)]
66

7-
use lightning::chain::Filter;
8-
use lightning::sign::EntropySource;
9-
107
use bitcoin::blockdata::constants::{genesis_block, ChainHash};
118
use bitcoin::blockdata::transaction::Transaction;
9+
use bitcoin::secp256k1::SecretKey;
1210
use bitcoin::Network;
11+
1312
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
13+
use lightning::chain::Filter;
1414
use lightning::chain::{chainmonitor, BestBlock, Confirm};
1515
use lightning::ln::channelmanager;
1616
use lightning::ln::channelmanager::ChainParameters;
@@ -24,6 +24,7 @@ use lightning::onion_message::messenger::DefaultMessageRouter;
2424
use lightning::routing::gossip::{NetworkGraph, P2PGossipSync};
2525
use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path};
2626
use lightning::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate};
27+
use lightning::sign::EntropySource;
2728
use lightning::sign::{InMemorySigner, KeysManager};
2829
use lightning::util::config::UserConfig;
2930
use lightning::util::persist::{
@@ -34,10 +35,13 @@ use lightning::util::persist::{
3435
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
3536
};
3637
use lightning::util::test_utils;
38+
39+
use lightning_liquidity::lsps5::service::TimeProvider;
3740
use lightning_liquidity::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig};
3841
use lightning_persister::fs_store::FilesystemStore;
3942

4043
use std::collections::{HashMap, VecDeque};
44+
use std::ops::Deref;
4145
use std::path::PathBuf;
4246
use std::sync::atomic::AtomicBool;
4347
use std::sync::mpsc::SyncSender;
@@ -67,7 +71,7 @@ type LockingWrapper<T> = lightning::routing::scoring::MultiThreadedLockableScore
6771
#[cfg(not(c_bindings))]
6872
type LockingWrapper<T> = std::sync::Mutex<T>;
6973

70-
type ChannelManager = channelmanager::ChannelManager<
74+
pub(crate) type ChannelManager = channelmanager::ChannelManager<
7175
Arc<ChainMonitor>,
7276
Arc<test_utils::TestBroadcaster>,
7377
Arc<KeysManager>,
@@ -127,13 +131,20 @@ pub(crate) struct Node {
127131
Arc<KeysManager>,
128132
Arc<ChannelManager>,
129133
Arc<dyn Filter + Send + Sync>,
134+
Arc<dyn TimeProvider>,
130135
>,
131136
>,
132137
Arc<KeysManager>,
133138
>,
134139
>,
135-
pub(crate) liquidity_manager:
136-
Arc<LiquidityManager<Arc<KeysManager>, Arc<ChannelManager>, Arc<dyn Filter + Send + Sync>>>,
140+
pub(crate) liquidity_manager: Arc<
141+
LiquidityManager<
142+
Arc<KeysManager>,
143+
Arc<ChannelManager>,
144+
Arc<dyn Filter + Send + Sync>,
145+
Arc<dyn TimeProvider>,
146+
>,
147+
>,
137148
pub(crate) chain_monitor: Arc<ChainMonitor>,
138149
pub(crate) kv_store: Arc<FilesystemStore>,
139150
pub(crate) tx_broadcaster: Arc<test_utils::TestBroadcaster>,
@@ -400,7 +411,7 @@ fn get_full_filepath(filepath: String, filename: String) -> String {
400411

401412
pub(crate) fn create_liquidity_node(
402413
i: usize, persist_dir: &str, network: Network, service_config: Option<LiquidityServiceConfig>,
403-
client_config: Option<LiquidityClientConfig>,
414+
client_config: Option<LiquidityClientConfig>, time_provider: Arc<dyn TimeProvider>,
404415
) -> Node {
405416
let tx_broadcaster = Arc::new(test_utils::TestBroadcaster::new(network));
406417
let fee_estimator = Arc::new(test_utils::TestFeeEstimator::new(253));
@@ -451,16 +462,16 @@ pub(crate) fn create_liquidity_node(
451462
Some(chain_source.clone()),
452463
logger.clone(),
453464
));
454-
455-
let liquidity_manager = Arc::new(LiquidityManager::new(
456-
Arc::clone(&keys_manager),
457-
Arc::clone(&channel_manager),
465+
let liquidity_manager = Arc::new(LiquidityManager::new_with_custom_time_provider(
466+
keys_manager.clone(),
467+
channel_manager.clone(),
458468
None::<Arc<dyn Filter + Send + Sync>>,
459-
Some(chain_params),
469+
Some(chain_params.clone()),
460470
service_config,
461471
client_config,
462-
None,
472+
time_provider,
463473
));
474+
464475
let msg_handler = MessageHandler {
465476
chan_handler: Arc::new(test_utils::TestChannelMessageHandler::new(
466477
ChainHash::using_genesis_block(Network::Testnet),
@@ -489,14 +500,29 @@ pub(crate) fn create_liquidity_node(
489500
}
490501

491502
pub(crate) fn create_service_and_client_nodes(
492-
persist_dir: &str, service_config: LiquidityServiceConfig, client_config: LiquidityClientConfig,
503+
persist_dir: &str, service_config: LiquidityServiceConfig,
504+
client_config: LiquidityClientConfig, time_provider: Arc<dyn TimeProvider>,
493505
) -> (Node, Node) {
494506
let persist_temp_path = env::temp_dir().join(persist_dir);
495507
let persist_dir = persist_temp_path.to_string_lossy().to_string();
496508
let network = Network::Bitcoin;
497509

498-
let service_node = create_liquidity_node(1, &persist_dir, network, Some(service_config), None);
499-
let client_node = create_liquidity_node(2, &persist_dir, network, None, Some(client_config));
510+
let service_node = create_liquidity_node(
511+
1,
512+
&persist_dir,
513+
network,
514+
Some(service_config),
515+
None,
516+
time_provider.clone(),
517+
);
518+
let client_node = create_liquidity_node(
519+
2,
520+
&persist_dir,
521+
network,
522+
None,
523+
Some(client_config),
524+
time_provider.clone(),
525+
);
500526

501527
service_node
502528
.channel_manager

0 commit comments

Comments
 (0)