Skip to content

Commit 1ceee2e

Browse files
committed
Add test asserting LSPS2 service state is persisted across restarts
We add a simple test that runs the LSPS2 flow, persists, and ensures we recover the service state after reinitializing from our `KVStore`.
1 parent 2c28d7f commit 1ceee2e

File tree

2 files changed

+236
-10
lines changed

2 files changed

+236
-10
lines changed

lightning-liquidity/tests/common/mod.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ pub(crate) struct LSPSNodes<'a, 'b, 'c> {
1919
pub client_node: LiquidityNode<'a, 'b, 'c>,
2020
}
2121

22-
pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>(
22+
pub(crate) fn create_service_and_client_nodes_with_kv_stores<'a, 'b, 'c>(
2323
nodes: Vec<Node<'a, 'b, 'c>>, service_config: LiquidityServiceConfig,
2424
client_config: LiquidityClientConfig, time_provider: Arc<dyn TimeProvider + Send + Sync>,
25+
service_kv_store: Arc<TestStore>, client_kv_store: Arc<TestStore>,
2526
) -> LSPSNodes<'a, 'b, 'c> {
2627
let chain_params = ChainParameters {
2728
network: Network::Testnet,
2829
best_block: BestBlock::from_network(Network::Testnet),
2930
};
30-
let service_kv_store = Arc::new(TestStore::new(false));
3131
let service_lm = LiquidityManagerSync::new_with_custom_time_provider(
3232
nodes[0].keys_manager,
3333
nodes[0].keys_manager,
@@ -41,7 +41,6 @@ pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>(
4141
)
4242
.unwrap();
4343

44-
let client_kv_store = Arc::new(TestStore::new(false));
4544
let client_lm = LiquidityManagerSync::new_with_custom_time_provider(
4645
nodes[1].keys_manager,
4746
nodes[1].keys_manager,
@@ -62,6 +61,23 @@ pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>(
6261
LSPSNodes { service_node, client_node }
6362
}
6463

64+
#[allow(unused)]
65+
pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>(
66+
nodes: Vec<Node<'a, 'b, 'c>>, service_config: LiquidityServiceConfig,
67+
client_config: LiquidityClientConfig, time_provider: Arc<dyn TimeProvider + Send + Sync>,
68+
) -> LSPSNodes<'a, 'b, 'c> {
69+
let service_kv_store = Arc::new(TestStore::new(false));
70+
let client_kv_store = Arc::new(TestStore::new(false));
71+
create_service_and_client_nodes_with_kv_stores(
72+
nodes,
73+
service_config,
74+
client_config,
75+
time_provider,
76+
service_kv_store,
77+
client_kv_store,
78+
)
79+
}
80+
6581
pub(crate) struct LiquidityNode<'a, 'b, 'c> {
6682
pub inner: Node<'a, 'b, 'c>,
6783
pub liquidity_manager: LiquidityManagerSync<

lightning-liquidity/tests/lsps2_integration_tests.rs

Lines changed: 217 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
mod common;
44

5-
use common::{create_service_and_client_nodes, get_lsps_message, LSPSNodes, LiquidityNode};
5+
use common::{
6+
create_service_and_client_nodes_with_kv_stores, get_lsps_message, LSPSNodes, LiquidityNode,
7+
};
68

79
use lightning_liquidity::events::LiquidityEvent;
810
use lightning_liquidity::lsps0::ser::LSPSDateTime;
@@ -12,10 +14,11 @@ use lightning_liquidity::lsps2::event::LSPS2ServiceEvent;
1214
use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams;
1315
use lightning_liquidity::lsps2::service::LSPS2ServiceConfig;
1416
use lightning_liquidity::lsps2::utils::is_valid_opening_fee_params;
15-
use lightning_liquidity::utils::time::DefaultTimeProvider;
16-
use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig};
17+
use lightning_liquidity::utils::time::{DefaultTimeProvider, TimeProvider};
18+
use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig};
1719

18-
use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA};
20+
use lightning::chain::{BestBlock, Filter};
21+
use lightning::ln::channelmanager::{ChainParameters, InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA};
1922
use lightning::ln::functional_test_utils::{
2023
create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs,
2124
};
@@ -26,6 +29,7 @@ use lightning::routing::router::{RouteHint, RouteHintHop};
2629
use lightning::sign::NodeSigner;
2730
use lightning::util::errors::APIError;
2831
use lightning::util::logger::Logger;
32+
use lightning::util::test_utils::TestStore;
2933

3034
use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees};
3135

@@ -42,8 +46,8 @@ use std::time::Duration;
4246
const MAX_PENDING_REQUESTS_PER_PEER: usize = 10;
4347
const MAX_TOTAL_PENDING_REQUESTS: usize = 1000;
4448

45-
fn setup_test_lsps2_nodes<'a, 'b, 'c>(
46-
nodes: Vec<Node<'a, 'b, 'c>>,
49+
fn setup_test_lsps2_nodes_with_kv_stores<'a, 'b, 'c>(
50+
nodes: Vec<Node<'a, 'b, 'c>>, service_kv_store: Arc<TestStore>, client_kv_store: Arc<TestStore>,
4751
) -> (LSPSNodes<'a, 'b, 'c>, [u8; 32]) {
4852
let promise_secret = [42; 32];
4953
let lsps2_service_config = LSPS2ServiceConfig { promise_secret };
@@ -61,16 +65,26 @@ fn setup_test_lsps2_nodes<'a, 'b, 'c>(
6165
lsps2_client_config: Some(lsps2_client_config),
6266
lsps5_client_config: None,
6367
};
64-
let lsps_nodes = create_service_and_client_nodes(
68+
let lsps_nodes = create_service_and_client_nodes_with_kv_stores(
6569
nodes,
6670
service_config,
6771
client_config,
6872
Arc::new(DefaultTimeProvider),
73+
service_kv_store,
74+
client_kv_store,
6975
);
7076

7177
(lsps_nodes, promise_secret)
7278
}
7379

80+
fn setup_test_lsps2_nodes<'a, 'b, 'c>(
81+
nodes: Vec<Node<'a, 'b, 'c>>,
82+
) -> (LSPSNodes<'a, 'b, 'c>, [u8; 32]) {
83+
let service_kv_store = Arc::new(TestStore::new(false));
84+
let client_kv_store = Arc::new(TestStore::new(false));
85+
setup_test_lsps2_nodes_with_kv_stores(nodes, service_kv_store, client_kv_store)
86+
}
87+
7488
fn create_jit_invoice(
7589
node: &LiquidityNode<'_, '_, '_>, service_node_id: PublicKey, intercept_scid: u64,
7690
cltv_expiry_delta: u32, payment_size_msat: Option<u64>, description: &str, expiry_secs: u32,
@@ -887,3 +901,199 @@ fn opening_fee_params_menu_is_sorted_by_spec() {
887901
panic!("Unexpected event");
888902
}
889903
}
904+
905+
#[test]
906+
fn lsps2_service_handler_persistence_across_restarts() {
907+
let chanmon_cfgs = create_chanmon_cfgs(2);
908+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
909+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
910+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
911+
912+
// Create shared KV store for service node that will persist across restarts
913+
let service_kv_store = Arc::new(TestStore::new(false));
914+
let client_kv_store = Arc::new(TestStore::new(false));
915+
916+
let promise_secret = [42; 32];
917+
let service_config = LiquidityServiceConfig {
918+
#[cfg(lsps1_service)]
919+
lsps1_service_config: None,
920+
lsps2_service_config: Some(LSPS2ServiceConfig { promise_secret }),
921+
lsps5_service_config: None,
922+
advertise_service: true,
923+
};
924+
let time_provider: Arc<dyn TimeProvider + Send + Sync> = Arc::new(DefaultTimeProvider);
925+
926+
// Variables to carry state between scopes
927+
let user_channel_id = 42;
928+
let cltv_expiry_delta = 144;
929+
let intercept_scid;
930+
let client_node_id;
931+
932+
// First scope: Setup, persistence, and dropping of all node objects
933+
{
934+
// Use the helper function with custom KV stores
935+
let (lsps_nodes, _) = setup_test_lsps2_nodes_with_kv_stores(
936+
nodes,
937+
Arc::clone(&service_kv_store),
938+
client_kv_store,
939+
);
940+
let LSPSNodes { service_node, client_node } = lsps_nodes;
941+
942+
let service_node_id = service_node.inner.node.get_our_node_id();
943+
client_node_id = client_node.inner.node.get_our_node_id();
944+
945+
let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap();
946+
let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap();
947+
948+
// Set up a JIT channel request to create state that needs persistence
949+
let _get_info_request_id = client_handler.request_opening_params(service_node_id, None);
950+
let get_info_request = get_lsps_message!(client_node, service_node_id);
951+
service_node
952+
.liquidity_manager
953+
.handle_custom_message(get_info_request, client_node_id)
954+
.unwrap();
955+
956+
let get_info_event = service_node.liquidity_manager.next_event().unwrap();
957+
let request_id = match get_info_event {
958+
LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { request_id, .. }) => {
959+
request_id
960+
},
961+
_ => panic!("Unexpected event"),
962+
};
963+
964+
let raw_opening_params = LSPS2RawOpeningFeeParams {
965+
min_fee_msat: 100,
966+
proportional: 21,
967+
valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(),
968+
min_lifetime: 144,
969+
max_client_to_self_delay: 128,
970+
min_payment_size_msat: 1,
971+
max_payment_size_msat: 100_000_000,
972+
};
973+
974+
service_handler
975+
.opening_fee_params_generated(
976+
&client_node_id,
977+
request_id.clone(),
978+
vec![raw_opening_params],
979+
)
980+
.unwrap();
981+
982+
let get_info_response = get_lsps_message!(service_node, client_node_id);
983+
client_node
984+
.liquidity_manager
985+
.handle_custom_message(get_info_response, service_node_id)
986+
.unwrap();
987+
988+
let opening_fee_params = match client_node.liquidity_manager.next_event().unwrap() {
989+
LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady {
990+
opening_fee_params_menu,
991+
..
992+
}) => opening_fee_params_menu.first().unwrap().clone(),
993+
_ => panic!("Unexpected event"),
994+
};
995+
996+
// Client makes a buy request
997+
let payment_size_msat = Some(1_000_000);
998+
let buy_request_id = client_handler
999+
.select_opening_params(service_node_id, payment_size_msat, opening_fee_params.clone())
1000+
.unwrap();
1001+
1002+
let buy_request = get_lsps_message!(client_node, service_node_id);
1003+
service_node.liquidity_manager.handle_custom_message(buy_request, client_node_id).unwrap();
1004+
1005+
let buy_event = service_node.liquidity_manager.next_event().unwrap();
1006+
if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { request_id, .. }) =
1007+
buy_event
1008+
{
1009+
assert_eq!(request_id, buy_request_id);
1010+
} else {
1011+
panic!("Unexpected event");
1012+
}
1013+
1014+
// Service responds with invoice parameters, creating persistent channel state
1015+
intercept_scid = service_node.node.get_intercept_scid();
1016+
let client_trusts_lsp = true;
1017+
1018+
service_handler
1019+
.invoice_parameters_generated(
1020+
&client_node_id,
1021+
buy_request_id.clone(),
1022+
intercept_scid,
1023+
cltv_expiry_delta,
1024+
client_trusts_lsp,
1025+
user_channel_id,
1026+
)
1027+
.unwrap();
1028+
1029+
let buy_response = get_lsps_message!(service_node, client_node_id);
1030+
client_node.liquidity_manager.handle_custom_message(buy_response, service_node_id).unwrap();
1031+
1032+
let _invoice_params_event = client_node.liquidity_manager.next_event().unwrap();
1033+
1034+
// Trigger persistence by calling persist
1035+
service_node.liquidity_manager.persist().unwrap();
1036+
1037+
// All node objects are dropped at the end of this scope
1038+
}
1039+
1040+
// Second scope: Recovery from persisted store and verification
1041+
{
1042+
// Create fresh node configurations for restart to avoid connection conflicts
1043+
let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1044+
let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart);
1045+
1046+
// Create a new LiquidityManager with the same configuration and KV store to simulate restart
1047+
let chain_params = ChainParameters {
1048+
network: Network::Testnet,
1049+
best_block: BestBlock::from_network(Network::Testnet),
1050+
};
1051+
1052+
let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider(
1053+
nodes_restart[0].keys_manager,
1054+
nodes_restart[0].keys_manager,
1055+
nodes_restart[0].node,
1056+
None::<Arc<dyn Filter + Send + Sync>>,
1057+
Some(chain_params),
1058+
service_kv_store,
1059+
Some(service_config),
1060+
None,
1061+
time_provider,
1062+
)
1063+
.unwrap();
1064+
1065+
let restarted_service_handler = restarted_service_lm.lsps2_service_handler().unwrap();
1066+
1067+
// Verify the state was properly restored by checking if the channel exists
1068+
// We can do this by trying to call htlc_intercepted which should succeed if state was restored
1069+
let htlc_amount_msat = 1_000_000;
1070+
let intercept_id = InterceptId([0; 32]);
1071+
let payment_hash = PaymentHash([1; 32]);
1072+
1073+
let result = restarted_service_handler.htlc_intercepted(
1074+
intercept_scid,
1075+
intercept_id,
1076+
htlc_amount_msat,
1077+
payment_hash,
1078+
);
1079+
1080+
// This should succeed if the channel state was properly restored
1081+
assert!(result.is_ok(), "HTLC interception should succeed with restored state");
1082+
1083+
// Check that we get an OpenChannel event, confirming the state was restored correctly
1084+
let event = restarted_service_lm.next_event();
1085+
assert!(event.is_some(), "Should have an event after HTLC interception");
1086+
1087+
if let Some(LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel {
1088+
user_channel_id: restored_channel_id,
1089+
intercept_scid: restored_scid,
1090+
..
1091+
})) = event
1092+
{
1093+
assert_eq!(restored_channel_id, user_channel_id);
1094+
assert_eq!(restored_scid, intercept_scid);
1095+
} else {
1096+
panic!("Expected OpenChannel event after restart");
1097+
}
1098+
}
1099+
}

0 commit comments

Comments
 (0)