Skip to content

Commit afa8492

Browse files
Add tests for LSPS5 client and service.
1 parent d7d9aa4 commit afa8492

File tree

6 files changed

+1528
-22
lines changed

6 files changed

+1528
-22
lines changed

lightning-background-processor/src/lib.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput};
648648
/// # use std::sync::atomic::{AtomicBool, Ordering};
649649
/// # use std::time::SystemTime;
650650
/// # use lightning_background_processor::{process_events_async, GossipSync};
651+
/// # use lightning_liquidity::lsps5::service::TimeProvider;
651652
/// # struct Logger {}
652653
/// # impl lightning::util::logger::Logger for Logger {
653654
/// # fn log(&self, _record: lightning::util::logger::Record) {}
@@ -659,6 +660,16 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput};
659660
/// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) }
660661
/// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result<Vec<String>> { Ok(Vec::new()) }
661662
/// # }
663+
/// #
664+
/// # use core::time::Duration;
665+
/// # struct DefaultTimeProvider;
666+
/// #
667+
/// # impl TimeProvider for DefaultTimeProvider {
668+
/// # fn duration_since_epoch(&self) -> Duration {
669+
/// # use std::time::{SystemTime, UNIX_EPOCH};
670+
/// # SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch")
671+
/// # }
672+
/// # }
662673
/// # struct EventHandler {}
663674
/// # impl EventHandler {
664675
/// # async fn handle_event(&self, _: lightning::events::Event) -> Result<(), ReplayEvent> { Ok(()) }
@@ -674,7 +685,7 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput};
674685
/// # type P2PGossipSync<UL> = lightning::routing::gossip::P2PGossipSync<Arc<NetworkGraph>, Arc<UL>, Arc<Logger>>;
675686
/// # type ChannelManager<B, F, FE> = lightning::ln::channelmanager::SimpleArcChannelManager<ChainMonitor<B, F, FE>, B, FE, Logger>;
676687
/// # 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>;
677-
/// # type LiquidityManager<B, F, FE> = lightning_liquidity::LiquidityManager<Arc<lightning::sign::KeysManager>, Arc<ChannelManager<B, F, FE>>, Arc<F>>;
688+
/// # type LiquidityManager<B, F, FE> = lightning_liquidity::LiquidityManager<Arc<lightning::sign::KeysManager>, Arc<ChannelManager<B, F, FE>>, Arc<F>, Arc<DefaultTimeProvider>>;
678689
/// # type Scorer = RwLock<lightning::routing::scoring::ProbabilisticScorer<Arc<NetworkGraph>, Arc<Logger>>>;
679690
/// # type PeerManager<B, F, FE, UL> = lightning::ln::peer_handler::SimpleArcPeerManager<SocketDescriptor, ChainMonitor<B, F, FE>, B, FE, Arc<UL>, Logger, F, Store>;
680691
/// #
@@ -1293,8 +1304,12 @@ mod tests {
12931304
IgnoringMessageHandler,
12941305
>;
12951306

1296-
type LM =
1297-
LiquidityManager<Arc<KeysManager>, Arc<ChannelManager>, Arc<dyn Filter + Sync + Send>>;
1307+
type LM = LiquidityManager<
1308+
Arc<KeysManager>,
1309+
Arc<ChannelManager>,
1310+
Arc<dyn Filter + Sync + Send>,
1311+
Arc<DefaultTimeProvider>,
1312+
>;
12981313

12991314
struct Node {
13001315
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: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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)]
@@ -9,8 +9,11 @@ use lightning::sign::{EntropySource, NodeSigner};
99

1010
use bitcoin::blockdata::constants::{genesis_block, ChainHash};
1111
use bitcoin::blockdata::transaction::Transaction;
12+
use bitcoin::secp256k1::SecretKey;
1213
use bitcoin::Network;
14+
1315
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
16+
use lightning::chain::Filter;
1417
use lightning::chain::{chainmonitor, BestBlock, Confirm};
1518
use lightning::ln::channelmanager;
1619
use lightning::ln::channelmanager::ChainParameters;
@@ -24,6 +27,7 @@ use lightning::onion_message::messenger::DefaultMessageRouter;
2427
use lightning::routing::gossip::{NetworkGraph, P2PGossipSync};
2528
use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path};
2629
use lightning::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate};
30+
use lightning::sign::EntropySource;
2731
use lightning::sign::{InMemorySigner, KeysManager};
2832
use lightning::util::config::UserConfig;
2933
use lightning::util::persist::{
@@ -34,10 +38,13 @@ use lightning::util::persist::{
3438
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
3539
};
3640
use lightning::util::test_utils;
41+
42+
use lightning_liquidity::lsps5::service::TimeProvider;
3743
use lightning_liquidity::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig};
3844
use lightning_persister::fs_store::FilesystemStore;
3945

4046
use std::collections::{HashMap, VecDeque};
47+
use std::ops::Deref;
4148
use std::path::PathBuf;
4249
use std::sync::atomic::AtomicBool;
4350
use std::sync::mpsc::SyncSender;
@@ -128,14 +135,21 @@ pub(crate) struct Node {
128135
Arc<KeysManager>,
129136
Arc<ChannelManager>,
130137
Arc<dyn Filter + Send + Sync>,
138+
Arc<dyn TimeProvider>,
131139
>,
132140
>,
133141
Arc<KeysManager>,
134142
Arc<ChainMonitor>,
135143
>,
136144
>,
137-
pub(crate) liquidity_manager:
138-
Arc<LiquidityManager<Arc<KeysManager>, Arc<ChannelManager>, Arc<dyn Filter + Send + Sync>>>,
145+
pub(crate) liquidity_manager: Arc<
146+
LiquidityManager<
147+
Arc<KeysManager>,
148+
Arc<ChannelManager>,
149+
Arc<dyn Filter + Send + Sync>,
150+
Arc<dyn TimeProvider>,
151+
>,
152+
>,
139153
pub(crate) chain_monitor: Arc<ChainMonitor>,
140154
pub(crate) kv_store: Arc<FilesystemStore>,
141155
pub(crate) tx_broadcaster: Arc<test_utils::TestBroadcaster>,
@@ -402,7 +416,7 @@ fn get_full_filepath(filepath: String, filename: String) -> String {
402416

403417
pub(crate) fn create_liquidity_node(
404418
i: usize, persist_dir: &str, network: Network, service_config: Option<LiquidityServiceConfig>,
405-
client_config: Option<LiquidityClientConfig>,
419+
client_config: Option<LiquidityClientConfig>, time_provider: Arc<dyn TimeProvider>,
406420
) -> Node {
407421
let tx_broadcaster = Arc::new(test_utils::TestBroadcaster::new(network));
408422
let fee_estimator = Arc::new(test_utils::TestFeeEstimator::new(253));
@@ -455,16 +469,16 @@ pub(crate) fn create_liquidity_node(
455469
Some(chain_source.clone()),
456470
logger.clone(),
457471
));
458-
459-
let liquidity_manager = Arc::new(LiquidityManager::new(
460-
Arc::clone(&keys_manager),
461-
Arc::clone(&channel_manager),
472+
let liquidity_manager = Arc::new(LiquidityManager::new_with_custom_time_provider(
473+
keys_manager.clone(),
474+
channel_manager.clone(),
462475
None::<Arc<dyn Filter + Send + Sync>>,
463-
Some(chain_params),
476+
Some(chain_params.clone()),
464477
service_config,
465478
client_config,
466-
None,
479+
time_provider,
467480
));
481+
468482
let msg_handler = MessageHandler {
469483
chan_handler: Arc::new(test_utils::TestChannelMessageHandler::new(
470484
ChainHash::using_genesis_block(Network::Testnet),
@@ -494,14 +508,29 @@ pub(crate) fn create_liquidity_node(
494508
}
495509

496510
pub(crate) fn create_service_and_client_nodes(
497-
persist_dir: &str, service_config: LiquidityServiceConfig, client_config: LiquidityClientConfig,
511+
persist_dir: &str, service_config: LiquidityServiceConfig,
512+
client_config: LiquidityClientConfig, time_provider: Arc<dyn TimeProvider>,
498513
) -> (Node, Node) {
499514
let persist_temp_path = env::temp_dir().join(persist_dir);
500515
let persist_dir = persist_temp_path.to_string_lossy().to_string();
501516
let network = Network::Bitcoin;
502517

503-
let service_node = create_liquidity_node(1, &persist_dir, network, Some(service_config), None);
504-
let client_node = create_liquidity_node(2, &persist_dir, network, None, Some(client_config));
518+
let service_node = create_liquidity_node(
519+
1,
520+
&persist_dir,
521+
network,
522+
Some(service_config),
523+
None,
524+
time_provider.clone(),
525+
);
526+
let client_node = create_liquidity_node(
527+
2,
528+
&persist_dir,
529+
network,
530+
None,
531+
Some(client_config),
532+
time_provider.clone(),
533+
);
505534

506535
service_node
507536
.channel_manager

0 commit comments

Comments
 (0)