Skip to content

Commit 2d9c5a0

Browse files
committed
Add end-to-end LSPS2 integration test for BOLT12 JIT channel flow
Add `bolt12_lsps2_end_to_end_test` that exercises the full BOLT12 + LSPS2 JIT channel lifecycle across three nodes (payer, service/LSP, client): - LSPS2 ceremony to negotiate JIT channel parameters - Client creates BOLT12 offer with `manually_handle_bolt12_invoice_requests` - Payer sends `InvoiceRequest` routed through the service via onion messages - Client handles `InvoiceRequestReceived` and calls `send_bolt12_invoice_for_intercept_scid` to create an invoice with blinded payment paths through the service's intercept SCID - Payer pays the invoice through the blinded path - Service intercepts the HTLC and opens a JIT channel to the client - Client claims, service broadcasts the funding transaction Co-Authored-By: HAL 9000
1 parent bacd6ae commit 2d9c5a0

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed

lightning-liquidity/tests/lsps2_integration_tests.rs

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ use common::{
77
get_lsps_message, LSPSNodes, LSPSNodesWithPayer, LiquidityNode,
88
};
99

10+
use lightning::blinded_path::IntroductionNode;
1011
use lightning::events::{ClosureReason, Event};
1112
use lightning::get_event_msg;
1213
use lightning::ln::channelmanager::{OptionalBolt11PaymentParams, PaymentId};
1314
use lightning::ln::functional_test_utils::*;
1415
use lightning::ln::msgs::BaseMessageHandler;
1516
use lightning::ln::msgs::ChannelMessageHandler;
1617
use lightning::ln::msgs::MessageSendEvent;
18+
use lightning::ln::msgs::OnionMessageHandler;
1719
use lightning::ln::types::ChannelId;
1820

1921
use lightning_liquidity::events::LiquidityEvent;
@@ -2000,6 +2002,304 @@ fn htlc_timeout_before_client_claim_results_in_handling_failed() {
20002002
payer_node.chain_monitor.added_monitors.lock().unwrap().clear();
20012003
}
20022004

2005+
#[test]
2006+
fn bolt12_lsps2_end_to_end_test() {
2007+
// End-to-end test of the BOLT12 + LSPS2 JIT channel flow. Three nodes: payer, service, client.
2008+
// client_trusts_lsp=true; funding transaction broadcast happens after client claims the HTLC.
2009+
//
2010+
// 1. Create a channel between payer and service
2011+
// 2. Do the LSPS2 ceremony between client and service to get an intercept SCID
2012+
// 3. Client creates a BOLT12 offer (with manually_handle_bolt12_invoice_requests = true)
2013+
// 4. Payer pays for the offer (sends InvoiceRequest via onion message)
2014+
// 5. Client receives InvoiceRequestReceived event and creates a BOLT12 invoice with blinded
2015+
// payment paths through the service's intercept SCID
2016+
// 6. Payer receives the invoice and sends payment through the blinded path
2017+
// 7. Service intercepts the HTLC and creates a JIT channel to the client
2018+
// 8. Service forwards the HTLC to the client via the JIT channel
2019+
// 9. Client claims the payment, service broadcasts the funding tx
2020+
let chanmon_cfgs = create_chanmon_cfgs(3);
2021+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
2022+
2023+
let mut service_node_config = test_default_channel_config();
2024+
service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8;
2025+
2026+
let mut client_node_config = test_default_channel_config();
2027+
client_node_config.manually_accept_inbound_channels = true;
2028+
client_node_config.channel_config.accept_underpaying_htlcs = true;
2029+
client_node_config.manually_handle_bolt12_invoice_requests = true;
2030+
2031+
let node_chanmgrs = create_node_chanmgrs(
2032+
3,
2033+
&node_cfgs,
2034+
&[Some(service_node_config), Some(client_node_config), None],
2035+
);
2036+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
2037+
let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes_with_payer(nodes);
2038+
let LSPSNodesWithPayer { ref service_node, ref client_node, ref payer_node } = lsps_nodes;
2039+
2040+
let payer_node_id = payer_node.node.get_our_node_id();
2041+
let service_node_id = service_node.inner.node.get_our_node_id();
2042+
let client_node_id = client_node.inner.node.get_our_node_id();
2043+
2044+
let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap();
2045+
2046+
// Create channel between payer and service
2047+
create_chan_between_nodes_with_value(&payer_node, &service_node.inner, 2_000_000, 100_000);
2048+
2049+
// LSPS2 ceremony: client negotiates JIT channel parameters with service
2050+
let intercept_scid = service_node.node.get_intercept_scid();
2051+
let user_channel_id = 42;
2052+
let cltv_expiry_delta: u32 = 144;
2053+
let payment_size_msat = Some(1_000_000);
2054+
let fee_base_msat = 1000;
2055+
2056+
execute_lsps2_dance(
2057+
&lsps_nodes,
2058+
intercept_scid,
2059+
user_channel_id,
2060+
cltv_expiry_delta,
2061+
promise_secret,
2062+
payment_size_msat,
2063+
fee_base_msat,
2064+
);
2065+
2066+
// Disconnect payer from client to ensure deterministic onion message routing through service.
2067+
// This guarantees that both the offer's blinded message paths and the payer's reply paths
2068+
// route through the service node.
2069+
payer_node.node.peer_disconnected(client_node_id);
2070+
client_node.node.peer_disconnected(payer_node_id);
2071+
payer_node.onion_messenger.peer_disconnected(client_node_id);
2072+
client_node.onion_messenger.peer_disconnected(payer_node_id);
2073+
2074+
// Client creates a BOLT12 offer. Since client's only remaining peer is service,
2075+
// the blinded message paths will use service as the introduction node.
2076+
let offer = client_node
2077+
.node
2078+
.create_offer_builder()
2079+
.unwrap()
2080+
.amount_msats(payment_size_msat.unwrap())
2081+
.build()
2082+
.unwrap();
2083+
2084+
// Payer initiates payment for the offer
2085+
let payment_id = PaymentId([1; 32]);
2086+
payer_node.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
2087+
2088+
// Route InvoiceRequest: payer -> service -> client
2089+
let onion_msg = payer_node
2090+
.onion_messenger
2091+
.next_onion_message_for_peer(service_node_id)
2092+
.expect("Payer should send InvoiceRequest toward service");
2093+
service_node.onion_messenger.handle_onion_message(payer_node_id, &onion_msg);
2094+
2095+
let fwd_msg = service_node
2096+
.onion_messenger
2097+
.next_onion_message_for_peer(client_node_id)
2098+
.expect("Service should forward InvoiceRequest to client");
2099+
client_node.onion_messenger.handle_onion_message(service_node_id, &fwd_msg);
2100+
2101+
// Client should NOT auto-create an invoice (manually_handle_bolt12_invoice_requests = true)
2102+
assert!(client_node.onion_messenger.next_onion_message_for_peer(service_node_id).is_none());
2103+
2104+
// Client receives InvoiceRequestReceived event
2105+
let mut events = client_node.node.get_and_clear_pending_events();
2106+
assert_eq!(events.len(), 1);
2107+
2108+
let (invoice_request, context, responder) = match events.pop().unwrap() {
2109+
Event::InvoiceRequestReceived { invoice_request, context, responder } => {
2110+
(invoice_request, context, responder)
2111+
},
2112+
other => panic!("Expected Event::InvoiceRequestReceived, got: {:?}", other),
2113+
};
2114+
2115+
// Client creates a BOLT12 invoice with blinded payment paths through the service's
2116+
// intercept SCID, simulating the LSPS2 JIT channel flow.
2117+
let invoice = client_node
2118+
.node
2119+
.send_bolt12_invoice_for_intercept_scid(
2120+
invoice_request,
2121+
context,
2122+
responder,
2123+
service_node_id,
2124+
intercept_scid,
2125+
cltv_expiry_delta as u16,
2126+
3600,
2127+
)
2128+
.unwrap();
2129+
2130+
// Verify the invoice has the correct structure: blinded payment paths with service
2131+
// as the introduction node (the LSP that will open the JIT channel).
2132+
assert_eq!(invoice.amount_msats(), payment_size_msat.unwrap());
2133+
assert!(!invoice.payment_paths().is_empty());
2134+
for path in invoice.payment_paths() {
2135+
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(service_node_id));
2136+
}
2137+
2138+
// Route Invoice: client -> service -> payer
2139+
let onion_msg = client_node
2140+
.onion_messenger
2141+
.next_onion_message_for_peer(service_node_id)
2142+
.expect("Client should send Invoice toward service");
2143+
service_node.onion_messenger.handle_onion_message(client_node_id, &onion_msg);
2144+
2145+
let fwd_msg = service_node
2146+
.onion_messenger
2147+
.next_onion_message_for_peer(payer_node_id)
2148+
.expect("Service should forward Invoice to payer");
2149+
payer_node.onion_messenger.handle_onion_message(service_node_id, &fwd_msg);
2150+
2151+
// Payer processes the invoice and starts the payment automatically
2152+
check_added_monitors(&payer_node, 1);
2153+
let events = payer_node.node.get_and_clear_pending_msg_events();
2154+
assert_eq!(events.len(), 1);
2155+
let ev = SendEvent::from_event(events[0].clone());
2156+
2157+
// Payment goes to service via the blinded payment path (through intercept SCID)
2158+
service_node.inner.node.handle_update_add_htlc(payer_node_id, &ev.msgs[0]);
2159+
do_commitment_signed_dance(&service_node.inner, &payer_node, &ev.commitment_msg, false, true);
2160+
service_node.inner.node.process_pending_htlc_forwards();
2161+
2162+
// Service intercepts the HTLC (destined for intercept SCID)
2163+
let events = service_node.inner.node.get_and_clear_pending_events();
2164+
assert_eq!(events.len(), 1);
2165+
let (payment_hash, expected_outbound_amount_msat) = match &events[0] {
2166+
Event::HTLCIntercepted {
2167+
intercept_id,
2168+
requested_next_hop_scid,
2169+
payment_hash,
2170+
expected_outbound_amount_msat,
2171+
..
2172+
} => {
2173+
assert_eq!(*requested_next_hop_scid, intercept_scid);
2174+
2175+
service_handler
2176+
.htlc_intercepted(
2177+
*requested_next_hop_scid,
2178+
*intercept_id,
2179+
*expected_outbound_amount_msat,
2180+
*payment_hash,
2181+
)
2182+
.unwrap();
2183+
(*payment_hash, expected_outbound_amount_msat)
2184+
},
2185+
other => panic!("Expected HTLCIntercepted event, got: {:?}", other),
2186+
};
2187+
2188+
// Service emits OpenChannel event with the correct fee deduction
2189+
let open_channel_event = service_node.liquidity_manager.next_event().unwrap();
2190+
match open_channel_event {
2191+
LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel {
2192+
their_network_key,
2193+
amt_to_forward_msat,
2194+
opening_fee_msat,
2195+
user_channel_id: uc_id,
2196+
intercept_scid: iscd,
2197+
}) => {
2198+
assert_eq!(their_network_key, client_node_id);
2199+
assert_eq!(amt_to_forward_msat, payment_size_msat.unwrap() - fee_base_msat);
2200+
assert_eq!(opening_fee_msat, fee_base_msat);
2201+
assert_eq!(uc_id, user_channel_id);
2202+
assert_eq!(iscd, intercept_scid);
2203+
},
2204+
other => panic!("Expected OpenChannel event, got: {:?}", other),
2205+
};
2206+
2207+
// Create JIT channel with manual broadcast (client_trusts_lsp = true)
2208+
let result =
2209+
service_handler.channel_needs_manual_broadcast(user_channel_id, &client_node_id).unwrap();
2210+
assert!(result, "Channel should require manual broadcast");
2211+
2212+
let (channel_id, funding_tx) = create_channel_with_manual_broadcast(
2213+
&service_node_id,
2214+
&client_node_id,
2215+
&service_node,
2216+
&client_node,
2217+
user_channel_id,
2218+
expected_outbound_amount_msat,
2219+
true,
2220+
);
2221+
2222+
// Service marks channel as ready and forwards the intercepted HTLC
2223+
service_handler.channel_ready(user_channel_id, &channel_id, &client_node_id).unwrap();
2224+
service_node.inner.node.process_pending_htlc_forwards();
2225+
2226+
// Forward the HTLC to client on the JIT channel
2227+
let pay_event = {
2228+
{
2229+
let mut added_monitors =
2230+
service_node.inner.chain_monitor.added_monitors.lock().unwrap();
2231+
assert_eq!(added_monitors.len(), 1);
2232+
added_monitors.clear();
2233+
}
2234+
let mut events = service_node.inner.node.get_and_clear_pending_msg_events();
2235+
assert_eq!(events.len(), 1);
2236+
SendEvent::from_event(events.remove(0))
2237+
};
2238+
2239+
client_node.inner.node.handle_update_add_htlc(service_node_id, &pay_event.msgs[0]);
2240+
do_commitment_signed_dance(
2241+
&client_node.inner,
2242+
&service_node.inner,
2243+
&pay_event.commitment_msg,
2244+
false,
2245+
true,
2246+
);
2247+
client_node.inner.node.process_pending_htlc_forwards();
2248+
2249+
// Client sees PaymentClaimable
2250+
let client_events = client_node.inner.node.get_and_clear_pending_events();
2251+
assert_eq!(client_events.len(), 1);
2252+
let preimage = match &client_events[0] {
2253+
Event::PaymentClaimable { payment_hash: ph, purpose, .. } => {
2254+
assert_eq!(*ph, payment_hash);
2255+
purpose.preimage()
2256+
},
2257+
other => panic!("Expected PaymentClaimable event on client, got: {:?}", other),
2258+
};
2259+
2260+
// Before client claims, service should not have broadcasted the funding tx
2261+
let broadcasted = service_node.inner.tx_broadcaster.txn_broadcasted.lock().unwrap();
2262+
assert!(broadcasted.is_empty(), "There should be no broadcasted txs yet");
2263+
drop(broadcasted);
2264+
2265+
// Client claims the payment
2266+
client_node.inner.node.claim_funds(preimage.unwrap());
2267+
2268+
claim_and_assert_forwarded_only(
2269+
&payer_node,
2270+
&service_node.inner,
2271+
&client_node.inner,
2272+
preimage.unwrap(),
2273+
);
2274+
2275+
// Service should emit PaymentForwarded
2276+
let service_events = service_node.node.get_and_clear_pending_events();
2277+
assert_eq!(service_events.len(), 1);
2278+
2279+
let total_fee_msat = match service_events[0].clone() {
2280+
Event::PaymentForwarded {
2281+
prev_node_id,
2282+
next_node_id,
2283+
skimmed_fee_msat,
2284+
total_fee_earned_msat,
2285+
..
2286+
} => {
2287+
assert_eq!(prev_node_id, Some(payer_node_id));
2288+
assert_eq!(next_node_id, Some(client_node_id));
2289+
service_handler.payment_forwarded(channel_id, skimmed_fee_msat.unwrap_or(0)).unwrap();
2290+
Some(total_fee_earned_msat.unwrap() - skimmed_fee_msat.unwrap())
2291+
},
2292+
_ => panic!("Expected PaymentForwarded event, got: {:?}", service_events[0]),
2293+
};
2294+
2295+
// Service should have broadcasted the funding tx after client claimed
2296+
let broadcasted = service_node.inner.tx_broadcaster.txn_broadcasted.lock().unwrap();
2297+
assert!(broadcasted.iter().any(|b| b.compute_txid() == funding_tx.compute_txid()));
2298+
2299+
// Payer should have the PaymentSent event
2300+
expect_payment_sent(&payer_node, preimage.unwrap(), Some(total_fee_msat), true, true);
2301+
}
2302+
20032303
fn claim_and_assert_forwarded_only<'a, 'b, 'c>(
20042304
payer_node: &lightning::ln::functional_test_utils::Node<'a, 'b, 'c>,
20052305
service_node: &lightning::ln::functional_test_utils::Node<'a, 'b, 'c>,

0 commit comments

Comments
 (0)