Skip to content

Commit 89af89d

Browse files
committed
Add test case asserting LSPS1ServiceState is persisted across restarts
Co-authored by Claude AI
1 parent d4c61a6 commit 89af89d

File tree

1 file changed

+257
-2
lines changed

1 file changed

+257
-2
lines changed

lightning-liquidity/tests/lsps1_integration_tests.rs

Lines changed: 257 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ use lightning_liquidity::lsps1::msgs::{
1515
};
1616
use lightning_liquidity::lsps1::service::LSPS1ServiceConfig;
1717
use lightning_liquidity::utils::time::DefaultTimeProvider;
18-
use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig};
18+
use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig};
1919

2020
use lightning::ln::functional_test_utils::{
2121
create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs,
2222
};
23-
use lightning::util::test_utils::TestStore;
23+
use lightning::util::test_utils::{TestBroadcaster, TestStore};
2424

25+
use bitcoin::secp256k1::PublicKey;
26+
use bitcoin::{Address, Network};
27+
28+
use std::str::FromStr;
2529
use std::sync::Arc;
2630

2731
use lightning::ln::functional_test_utils::{create_network, Node};
32+
use lightning_liquidity::lsps1::msgs::LSPS1OrderId;
33+
use lightning_liquidity::utils::time::TimeProvider;
2834

2935
fn build_lsps1_configs(
3036
supported_options: LSPS1Options,
@@ -240,3 +246,252 @@ fn lsps1_happy_path() {
240246
panic!("Unexpected event");
241247
}
242248
}
249+
250+
#[test]
251+
fn lsps1_service_handler_persistence_across_restarts() {
252+
let chanmon_cfgs = create_chanmon_cfgs(2);
253+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
254+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
255+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
256+
257+
// Create shared KV store for service node that will persist across restarts
258+
let service_kv_store = Arc::new(TestStore::new(false));
259+
let client_kv_store = Arc::new(TestStore::new(false));
260+
261+
let supported_options = LSPS1Options {
262+
min_required_channel_confirmations: 0,
263+
min_funding_confirms_within_blocks: 6,
264+
supports_zero_channel_reserve: true,
265+
max_channel_expiry_blocks: 144,
266+
min_initial_client_balance_sat: 10_000_000,
267+
max_initial_client_balance_sat: 100_000_000,
268+
min_initial_lsp_balance_sat: 100_000,
269+
max_initial_lsp_balance_sat: 100_000_000,
270+
min_channel_balance_sat: 100_000,
271+
max_channel_balance_sat: 100_000_000,
272+
};
273+
274+
let service_config = LiquidityServiceConfig {
275+
lsps1_service_config: Some(LSPS1ServiceConfig {
276+
supported_options: supported_options.clone(),
277+
token: None,
278+
}),
279+
lsps2_service_config: None,
280+
lsps5_service_config: None,
281+
advertise_service: true,
282+
};
283+
let time_provider: Arc<dyn TimeProvider + Send + Sync> = Arc::new(DefaultTimeProvider);
284+
285+
// Variables to carry state between scopes
286+
let client_node_id: PublicKey;
287+
let expected_order_id: LSPS1OrderId;
288+
let order_params: LSPS1OrderParams;
289+
let payment_info: LSPS1PaymentInfo;
290+
291+
// First scope: Setup, persistence, and dropping of all node objects
292+
{
293+
let LSPSNodes { service_node, client_node } = setup_test_lsps1_nodes_with_kv_stores(
294+
nodes,
295+
Arc::clone(&service_kv_store),
296+
client_kv_store,
297+
supported_options.clone(),
298+
);
299+
300+
let service_node_id = service_node.inner.node.get_our_node_id();
301+
client_node_id = client_node.inner.node.get_our_node_id();
302+
303+
let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap();
304+
let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap();
305+
306+
// Request supported options
307+
let _request_supported_options_id =
308+
client_handler.request_supported_options(service_node_id);
309+
let request_supported_options = get_lsps_message!(client_node, service_node_id);
310+
311+
service_node
312+
.liquidity_manager
313+
.handle_custom_message(request_supported_options, client_node_id)
314+
.unwrap();
315+
316+
let get_info_message = get_lsps_message!(service_node, client_node_id);
317+
client_node
318+
.liquidity_manager
319+
.handle_custom_message(get_info_message, service_node_id)
320+
.unwrap();
321+
322+
let _get_info_event = client_node.liquidity_manager.next_event().unwrap();
323+
324+
// Create an order to establish persistent state
325+
order_params = LSPS1OrderParams {
326+
lsp_balance_sat: 100_000,
327+
client_balance_sat: 10_000_000,
328+
required_channel_confirmations: 0,
329+
funding_confirms_within_blocks: 6,
330+
channel_expiry_blocks: 144,
331+
token: None,
332+
announce_channel: true,
333+
};
334+
335+
let refund_onchain_address =
336+
Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr")
337+
.unwrap()
338+
.assume_checked();
339+
let create_order_id = client_handler.create_order(
340+
&service_node_id,
341+
order_params.clone(),
342+
Some(refund_onchain_address),
343+
);
344+
let create_order = get_lsps_message!(client_node, service_node_id);
345+
346+
service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap();
347+
348+
let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap();
349+
let request_id =
350+
if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails {
351+
request_id,
352+
..
353+
}) = request_for_payment_event
354+
{
355+
request_id
356+
} else {
357+
panic!("Unexpected event");
358+
};
359+
360+
// Service sends payment details, creating persistent order state
361+
let json_str = r#"{
362+
"state": "EXPECT_PAYMENT",
363+
"expires_at": "2035-01-01T00:00:00Z",
364+
"fee_total_sat": "9999",
365+
"order_total_sat": "200999",
366+
"address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr",
367+
"min_onchain_payment_confirmations": 1,
368+
"min_fee_for_0conf": 253
369+
}"#;
370+
371+
let onchain: LSPS1OnchainPaymentInfo =
372+
serde_json::from_str(json_str).expect("Failed to parse JSON");
373+
payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) };
374+
service_handler
375+
.send_payment_details(request_id.clone(), client_node_id, payment_info.clone())
376+
.unwrap();
377+
378+
let create_order_response = get_lsps_message!(service_node, client_node_id);
379+
380+
client_node
381+
.liquidity_manager
382+
.handle_custom_message(create_order_response, service_node_id)
383+
.unwrap();
384+
385+
let order_created_event = client_node.liquidity_manager.next_event().unwrap();
386+
expected_order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated {
387+
request_id,
388+
order_id,
389+
..
390+
}) = order_created_event
391+
{
392+
assert_eq!(request_id, create_order_id);
393+
order_id
394+
} else {
395+
panic!("Unexpected event");
396+
};
397+
398+
// Trigger persistence by calling persist
399+
service_node.liquidity_manager.persist().unwrap();
400+
401+
// All node objects are dropped at the end of this scope
402+
}
403+
404+
// Second scope: Recovery from persisted store and verification
405+
{
406+
// Create fresh node configurations for restart
407+
let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
408+
let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart);
409+
410+
// Create a new LiquidityManager with the same configuration and KV store to simulate restart
411+
let service_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet));
412+
let client_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet));
413+
let client_kv_store_restart = Arc::new(TestStore::new(false));
414+
415+
let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider(
416+
nodes_restart[0].keys_manager,
417+
nodes_restart[0].keys_manager,
418+
nodes_restart[0].node,
419+
service_kv_store,
420+
service_transaction_broadcaster,
421+
Some(service_config),
422+
None,
423+
Arc::clone(&time_provider),
424+
)
425+
.unwrap();
426+
427+
// Create a fresh client to query the restarted service
428+
let lsps1_client_config = LSPS1ClientConfig { max_channel_fees_msat: None };
429+
let client_config = LiquidityClientConfig {
430+
lsps1_client_config: Some(lsps1_client_config),
431+
lsps2_client_config: None,
432+
lsps5_client_config: None,
433+
};
434+
435+
let client_lm = LiquidityManagerSync::new_with_custom_time_provider(
436+
nodes_restart[1].keys_manager,
437+
nodes_restart[1].keys_manager,
438+
nodes_restart[1].node,
439+
client_kv_store_restart,
440+
client_transaction_broadcaster,
441+
None,
442+
Some(client_config),
443+
time_provider,
444+
)
445+
.unwrap();
446+
447+
let service_node_id = nodes_restart[0].node.get_our_node_id();
448+
let client_node_id_restart = nodes_restart[1].node.get_our_node_id();
449+
450+
// Verify node IDs match (since we use same node_cfgs)
451+
assert_eq!(client_node_id_restart, client_node_id);
452+
453+
// Use the client to send a GetOrder request
454+
let client_handler = client_lm.lsps1_client_handler().unwrap();
455+
let check_order_status_id =
456+
client_handler.check_order_status(&service_node_id, expected_order_id.clone());
457+
458+
// Get the request message from client
459+
let pending_client_msgs = client_lm.get_and_clear_pending_msg();
460+
assert_eq!(pending_client_msgs.len(), 1);
461+
let (target_node_id, request_msg) = pending_client_msgs.into_iter().next().unwrap();
462+
assert_eq!(target_node_id, service_node_id);
463+
464+
// Pass the request to the restarted service
465+
restarted_service_lm.handle_custom_message(request_msg, client_node_id).unwrap();
466+
467+
// Get the response from the service
468+
let pending_service_msgs = restarted_service_lm.get_and_clear_pending_msg();
469+
assert_eq!(pending_service_msgs.len(), 1);
470+
let (target_node_id, response_msg) = pending_service_msgs.into_iter().next().unwrap();
471+
assert_eq!(target_node_id, client_node_id);
472+
473+
// Pass the response to the client
474+
client_lm.handle_custom_message(response_msg, service_node_id).unwrap();
475+
476+
// Verify the client receives the order status event with correct data
477+
let order_status_event = client_lm.next_event().unwrap();
478+
if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus {
479+
request_id,
480+
counterparty_node_id,
481+
order_id,
482+
order,
483+
payment,
484+
channel,
485+
}) = order_status_event
486+
{
487+
assert_eq!(request_id, check_order_status_id);
488+
assert_eq!(counterparty_node_id, service_node_id);
489+
assert_eq!(order_id, expected_order_id);
490+
assert_eq!(order, order_params);
491+
assert_eq!(payment, payment_info);
492+
assert!(channel.is_none());
493+
} else {
494+
panic!("Expected OrderStatus event after restart, got: {:?}", order_status_event);
495+
}
496+
}
497+
}

0 commit comments

Comments
 (0)